Rotating an array with slices and tuple assignment - python

I am trying to solve the LeetCode problem 189. Rotate Array in Python:
Given an array, rotate the array to the right by k steps, where k is non-negative.
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
The following code works:
n = len(nums)
k = k % n
nums[k:], nums[:k] = nums[:-k], nums[-k:]
But if I change the order of the assignments in the last line, i.e. replace it with
nums[:k], nums[k:] = nums[-k:], nums[:-k]
the code doesn't work for the case of k=0. Clearly in this case, nums[:k] and nums[:-k] would be empty and the sizes of the lists on the left hand side and the right hand side don't match, but somehow the first code works and the second one doesn't.
Can someone explain?

The order in which the tuple assignment is executed, is indeed relevant here.
Let's say nums is [1,2,3,4] (and k 0)
Then the non-working assignment comes down to this:
nums[:0], nums[0:] = [1,2,3,4], []
...which is executed in this order:
# First the right hand side expressions are evaluated
complete = [1,2,3,4]
empty = []
# Then these values are assigned to the left hand side
nums[:0] = complete
nums[0:] = empty
Note that the second phase has no magical simultaneous assignment: these assignments happen in a left-to-right order.
It is now clear that the latter assignment destroys the effect of the first. It is also clear if they were executed in the opposite order, the end result would be fine.
So the order of assignments matter, even in tuple assignments.
The deeper reason why the behaviour is different specifically when k is 0, is related to how -k works in a slice notation: this always counts abs(k) steps backwards from the end, except when k=0: then it represents the very first index.
You would not have this different behaviour if you would do this:
nums[:k], nums[k:] = nums[len(nums)-k:], nums[:len(nums)-k]
Now it is explicit that the index should be determined by stepping backwards from the end, even when k is 0. This tuple assignment can be swapped, and it will still work, also for k equal to 0.

Well, consider this:
nums = [1, 2, 3]
nums[:0], nums[0:] = [1, 2, 3], []
print(nums)
Given the next to last line, what would you expect nums to be? The problem is that the statement is ambiguous to begin with and it just happens to be that the second implementation gets you an undesirable result - it resets the entire list to [] after adding [1,2,3] to the start. The first implementation does it the other way around.
What you were really after:
nums = nums[k:] + nums[:k]

Related

Leetcode claiming incorrect answer when it's inarguably correct

I'm trying to complete the 189. Rotate Array problem on Leetcode.
This is the code I've written:
class Solution(object):
def rotate(self, nums, k):
end = len(nums) - 1
carry = []
new = [n for n in nums]
for i in range(end-k+1, end+1):
carry.append(nums[i])
for i in range(1, k+1):
new.pop()
for n in new:
carry.append(n)
print(carry)
return carry
(The obsolete variables were to try and erase any possibility of something wacky happening with Leetcode's testing system.)
I can run this code anywhere (I've tried VS Code and an online interpreter) and the result is always correct (for the first case, [5, 6, 7, 1, 2, 3, 4]).
However, when I try in Leetcode:
The stdout shows the correct answer literally the line before the return, and yet the array somehow magically changes through no tampering.
This is driving me up the wall... please help!!
(Problem description:
Given an array, rotate the array to the right by k steps, where k is non-negative.
Example 1:
Input: nums = [1,2,3,4,5,6,7], k = 3
Output: [5,6,7,1,2,3,4]
Explanation:
rotate 1 steps to the right: [7,1,2,3,4,5,6]
rotate 2 steps to the right: [6,7,1,2,3,4,5]
rotate 3 steps to the right: [5,6,7,1,2,3,4]
Example 2:
Input: nums = [-1,-100,3,99], k = 2
Output: [3,99,-1,-100]
Explanation:
rotate 1 steps to the right: [99,-1,-100,3]
rotate 2 steps to the right: [3,99,-1,-100]
Constraints:
1 <= nums.length <= 105
-231 <= nums[i] <= 231 - 1
0 <= k <= 105
Follow up:
Try to come up with as many solutions as you can. There are at least three different ways to solve this problem.
Could you do it in-place with O(1) extra space?
)
The Python template for this problem says:
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
As the docstring mentioned, you'll need to modify nums in-place. There is no need to print or return anything.
A simple fix to your existing code would be to use an assignment nums[:] = carry at the end.
To answer #mreyeball's question about why nums = carry doesn't work:
Lists are passed by reference. This means that if you pass a list to a method, the list inside the method will refer to the same memory location as the list outside of the method.
This means that changes inside the method will affect the list outside the method as well.
However, this only goes for changes to the elements of a list. When you do nums = carry, you are literally saying "The variable nums will now refer to the same list as carry". Therefore, there is no longer a link between nums and the array that was originally passed to the method.
foo = [1,2,3,4,5]
doStuff(foo)
...
def doStuff(arr):
arr[0] = 2 # this will affect 'foo' as well
bar = ['a','b','c'] # new array
arr = bar # link broken!
arr[0] = 'd' # this won't affect 'foo', but it will affect 'bar'.

Built in (remove) function not working with function variable

Have a good day everyone, pardon my lack of understanding, but I can't seem to figure out why python built in function does not work when being called with another function variable and it just doesn't do what I want at all. Here is the code
def ignoreten(h):
ignoring = False
for i in range (1,len(h)-2):
if ignoring == True and h[i]==10:
h.remove(10)
if ignoring == False and h[i] ==10:
ignoring = True
The basic idea of this is just to decided the first 10 in a list, keep it, continue iterating until you faced another 10, then just remove that 10 to avoid replication, I had searched around but can't seem to find any solution and that's why I have to bring it up here. Thank you
The code you listed
def ignoreten(h):
ignoring = False
for i in range (1,len(h)-2):
if ignoring == True and h[i]==10:
h.remove(10)
if ignoring == False and h[i] ==10:
ignoring = True
Will actually do almost the exact opposite of what you want. It'll iterate over h (sort of, see [1]), and if it finds 10 twice, it'll remove the first occurrence from the list. (And, if it finds 10 three times, it'll remove the first two occurrences from the list.)
Note that list.remove will:
Remove the first item from the list whose value is equal to x. It
raises a ValueError if there is no such item.
Also note that you're mutating the list you're iterating over, so there's some additional weirdness here which may be confusing you, depending on your input.
From your follow-up comment to my question, it looks like you want to remove only the second occurrence of 10, not the first and not any subsequent occurrences.
Here are a few ways:
Iterate, store index, use del
def ignoreten(h):
index = None
found_first = False
for i,v in enumerate(h):
if v == 10:
if not found_first:
found_first = True
else:
index = i
break
if index is not None:
del h[index]
A little more verbose than necessary, but explicit, safe, and modifiable without much fear.
Alternatively, you could delete inside the loop but you want to make sure you immediately break:
def ignoreten(h):
found_first = False
for i,v in enumerate(h):
if v == 10:
if not found_first:
found_first = True
else:
del h[i]
break
Collect indices of 10s, remove second
def ignoreten(h):
indices = [i for (i,v) in enumerate(h) if v == 10]
if len(indices) > 1:
del h[indices[1]] # The second index of 10 is at indices[1]
Clean, but will unnecessarily iterate past the second 10 and collect as many indices of 10s are there are. Not likely a huge issue, but worth pointing out.
Collect indices of 10s, remove second (v2, from comments)
def ignoreten(h):
indices = [i for (i,v) in enumerate(h) if v == 10]
for i in reversed(indices[1:]):
del h[i]
From your comment asking about removing all non-initial occurrences of 10, if you're looking for in-place modification of h, then you almost definitely want something like this.
The first line collects all the indices of 10 into a list.
The second line is a bit tricky, but working inside-out it:
[1:] "throws out" the first element of that list (since you want to keep the first occurrence of 10)
reversed iterates over that list backwards
del h[i] removes the values at those indices.
The reason we iterate backwards is because doing so won't invalidate the rest of our indices that we've yet to delete.
In other words, if the list h was [1, 10, 2, 10, 3, 10], our indices list would be [1, 3, 5].
In both cases we skip 1, fine.
But if we iterate forwards, once we delete 3, and our list shrinks to 5 elements, when we go to delete 5 we get an IndexError.
Even if we didn't go out of bounds to cause an IndexError, our elements would shift and we'd be deleting incorrect values.
So instead, we iterate backwards over our indices, delete 5, the list shrinks to 5 elements, and index 3 is still valid (and still 10).
With list.index
def ignoreten(h):
try:
second_ten = h.index(10, h.index(10)+1)
del h[second_ten]
except ValueError:
pass
The inner .index call finds the first occurrence, the second uses the optional begin parameter to start searching after that. Wrapped in try/except in case there are less than two occurrences.
⇒ Personally, I'd prefer these in the opposite order of how they're listed.
[1] You're iterating over a weird subset of the list with your arguments to range. You're skipping (not applying your "is 10" logic to) the first and last two elements this way.
Bonus: Walrus abuse
(don't do this)
def ignoreten(h):
x = 0
return [v for v in h if v != 10 or (x := x + 1) != 1]
(unlike the previous versions that operated on h in-place, this creates a new list without the second occurrence of 10)
But the walrus operator is contentious enough already, please don't let this code out in the wild. Really.

Python: Passing a list as parameter so the actual list is sorted

I wrote a merge sort function and thought I was done.. but in the assignment it says that the function is supposed to sort the actual list instead of creating a copy (so call by value instead of reference I think?). Right now, it doesn't work because the list itself isn't changed.
def mergeSort(L, ascending = True):
print('Mergesort, Parameter L:')
print(L)
result = []
if len(L) == 1:
return L
mid = len(L) // 2
teilliste1 = mergeSort(L[:mid], ascending)
teilliste2 = mergeSort(L[mid:], ascending)
x, y = 0, 0
while x < len(teilliste1) and y < len(teilliste2):
if (ascending and teilliste1[x] > teilliste2[y]) or (not ascending and teilliste1[x] < teilliste2[y]):
result.append(teilliste2[y])
y = y + 1
else:
result.append(teilliste1[x])
x = x + 1
result = result + teilliste1[x:]
result = result + teilliste2[y:]
return result
liste1 = list([3, 2, -1, 9, 17, 4, 1, 0])
mergeSort(liste1)
print(liste1) # result will be the unsorted list
What do I need to change in the function to make it call by value and sort the actual list?
I know I could do
mergeResult = mergeSort(liste1)
print(mergeResult)
but apparently I have to change the original parameter list.
There are two basic ways to write a recursive decomposition function. The immutable version calls itself on copies of two smaller parts, then reassembles and returns them; that's what you wrote. The mutable version calls itself on the actual input, then modifies that in-place, and returns nothing; that's what your teacher wants here.
Notice that, unlike some other sorting algorithms, mergesort can't be done with constant extra storage, only better than linear extra storage. (Logarithmic is possible, but complicated; I doubt your teacher is insisting on that.) And because of this, most mergesort algorithms you find in books, Wikipedia, etc. will be written as copying sorts rather than in-place sorts. Which means this is probably a bit of a "trick question", trying to see whether you can figure out how to convert from the well-known copying version of the algorithm into an in-place version with explicit extra storage.
You can always write an immutable algorithm and then mutate at the very end, e.g.:
def _mergesort(L, ascending):
# your existing code
def mergesort(L, ascending=True):
L[:] = _mergesort(L, ascending)
This gives you all the cost of immutability without the benefits. But it does mean you can write a variety of sort functions with the same API, which are all implemented in-place if that's a reasonable optimization, but not if it isn't, which seems to be what your teacher is after.
If you don't want a wrapper function, you can change the last line from:
return result
… to:
L[:] = result
However, because this changes the API, you also need to change your recursive calls to match. For example, you could do this:
teilliste1 = L[:mid]
mergeSort(teilliste1, ascending)
teilliste2 = L[mid:]
mergeSort(teilliste2, ascending)
In Python, a mutating recursive decomposition function often works by passing start and end indices down with the list, like this:
def mergesort(L, ascending=True, start=None, stop=None):
if start is None: start = 0
if stop is None: stop = len(L)
if stop - start <= 1:
return
mid = (stop - start) // 2 + start
mergeSort(L[start:mid], ascending)
mergeSort(L[mid:stop], ascending)
# etc.
As mentioned above, the merging step is going to require some auxiliary storage. The simplest thing to do—and probably good enough for your assignment, even though it means linear space—is to just build up a left list and a right list and then assign them back into L[start:mid], L[mid:stop] = left, right.
Notice that this isn't all that different from the L[:] = result version above; it's really just a matter of using L itself, together with start and stop indices, in place of copies for the first half of the process, and then copying only at the end during the merge.

using recursion to find the maximum in a list

I'm trying to find the maximum element in a list using recursion.
the input needs to be the actual list, the left index, and the right index.
I have written a function and can't understand why it won't work. I drew the recursion tree, ran examples of lists in my head, and it makes sense , that's why it's even harder to find a solution now ! (it's fighting myself basically).
I know it's ugly, but try to ignore that. my idea is to split the list in half at each recursive call (it's required), and while the left index will remain 0, the right will be the length of the new halved list, minus 1.
the first call to the function will be from the tail function.
thanks for any help, and I hope I'm not missing something really stupid, or even worse- not even close !
by the way- didn't use slicing to cut list because I'm not allowed.
def max22(L,left,right):
if len(L)==1:
return L[0]
a = max22([L[i] for i in range(left, (left+right)//2)], 0 , len([L[i] for i in range(left, (left+right)//2)])-1)
b = max22([L[i] for i in range(((left+right)//2)+1, right)], 0 ,len([L[i] for i in range(left, (left+right)//2)])-1)
return max(a,b)
def max_list22(L):
return max22(L,0,len(L)-1)
input example -
for max_list22([1,20,3]) the output will be 20.
First off, for the sake of clarity I suggest assigning your list comprehensions to variables so you don't have to write each one twice. This should make the code easier to debug. You can also do the same for the (left+right)//2 value.
def max22(L,left,right):
if len(L)==1:
return L[0]
mid = (left+right)//2
left_L = [L[i] for i in range(left, mid)]
right_L = [L[i] for i in range(mid+1, right)]
a = max22(left_L, 0 , len(left_L)-1)
b = max22(right_L, 0 , len(left_L)-1)
return max(a,b)
def max_list22(L):
return max22(L,0,len(L)-1)
print max_list22([4,8,15,16,23,42])
I see four problems with this code.
On your b = line, the second argument is using len(left_L) instead of len(right_L).
You're missing an element between left_L and right_L. You should not be adding one to mid in the right_L list comprehension.
You're missing the last element of the list. You should be going up to right+1 in right_L, not just right.
Your mid value is off by one in the case of even sized lists. Ex. [1,2,3,4] should split into [1,2] and [3,4], but with your mid value you're getting [1] and [2,3,4]. (assuming you've already fixed the missing element problems in the previous bullet points).
Fixing these problems looks like:
def max22(L,left,right):
if len(L)==1:
return L[0]
mid = (left+right+1)//2
left_L = [L[i] for i in range(left, mid)]
right_L = [L[i] for i in range(mid, right+1)]
a = max22(left_L, 0 , len(left_L)-1)
b = max22(right_L, 0 , len(right_L)-1)
return max(a,b)
def max_list22(L):
return max22(L,0,len(L)-1)
print max_list22([4,8,15,16,23,42])
And if you insist on not using temporary variables, it looks like:
def max22(L,left,right):
if len(L)==1:
return L[0]
a = max22([L[i] for i in range(left, (left+right+1)//2)], 0 , len([L[i] for i in range(left, (left+right+1)//2)])-1)
b = max22([L[i] for i in range((left+right+1)//2, right+1)], 0 , len([L[i] for i in range((left+right+1)//2, right+1)])-1)
return max(a,b)
def max_list22(L):
return max22(L,0,len(L)-1)
print max_list22([4,8,15,16,23,42])
Bonus style tip: you don't necessarily need three arguments for max22, since left is always zero and right is always the length of the list minus one.
def max22(L):
if len(L)==1:
return L[0]
mid = (len(L))//2
left_L = [L[i] for i in range(0, mid)]
right_L = [L[i] for i in range(mid, len(L))]
a = max22(left_L)
b = max22(right_L)
return max(a,b)
print max22([4,8,15,16,23,42])
The problem is that you aren't handling empty lists at all. max_list22([]) recurses infinitely, and [L[i] for i in range(((left+right)//2)+1, right)] eventually produces an empty list.
Your problem is that you don't handle uneven splits. Lists could become empty using your code, but you can also stop on sizes 1 and 2 instead of 0 and 1 whichi s more natural (because you return a max, zero size lists don't have a max).
def max22(L,left,right):
if left == right:
# handle size 1
return L[left]
if left + 1 == right:
# handle size 2
return max(L[left], L[right])
# split the lists (could be uneven lists!)
split_index = (left + right) / 2
# solve two easier problems
return max (max22(L, left, split_index), max22(L, split_index, right))
print max22([1,20, 3], 0, 2)
Notes:
Lose the list comprehension, you don't have to create new lists since you have indices within the list.
When dealing with recursion, you have to think of:
1 - the stop condition(s), in this case there are two because list splits can be uneven, making the recursion stop at uneven conditions.
2 - the easier problem step . Assuming I can solve an easier problem, how can I solve this problem? This is usually what's in the end of the recursive function. In this case a call to the same function on two smaller (index-wise) lists. Recursion looks a lot like proof by induction if you're familiar with it.
Python prefers things to be done explicitly. While Python has some functional features it's best to let readers of the code know what you're up to ratehr than having a big one-liner that makes people scratch their head.
Good luck!

for-in loop's upper limit changing in each loop

How can I update the upper limit of a loop in each iteration? In the following code, List is shortened in each loop. However, the lenList in the for, in loop is not, even though I defined lenList as global. Any ideas how to solve this? (I'm using Python 2.sthg)
Thanks!
def similarity(List):
import difflib
lenList = len(List)
for i in range(1,lenList):
import numpy as np
global lenList
a = List[i]
idx = [difflib.SequenceMatcher(None, a, x).ratio() for x in List]
z = idx > .9
del List[z]
lenList = len(List)
X = ['jim','jimmy','luke','john','jake','matt','steve','tj','pat','chad','don']
similarity(X)
Looping over indices is bad practice in python. You may be able to accomplish what you want like this though (edited for comments):
def similarity(alist):
position = 0
while position < len(alist):
item = alist[position]
position += 1
# code here that modifies alist
A list will evaluate True if it has any entries, or False when it is empty. In this way you can consume a list that may grow during the manipulation of its items.
Additionally, if you absolutely have to have indices, you can get those as well:
for idx, item in enumerate(alist):
# code here, where items are actual list entries, and
# idx is the 0-based index of the item in the list.
In ... 3.x (I believe) you can even pass an optional parameter to enumerate to control the starting value of idx.
The issue here is that range() is only evaluated once at the start of the loop and produces a range generator (or list in 2.x) at that time. You can't then change the range. Not to mention that numbers and immutable, so you are assigning a new value to lenList, but that wouldn't affect any uses of it.
The best solution is to change the way your algorithm works not to rely on this behaviour.
The range is an object which is constructed before the first iteration of your loop, so you are iterating over the values in that object. You would instead need to use a while loop, although as Lattyware and g.d.d.c point out, it would not be very Pythonic.
What you are effectively looping on in the above code is a list which got generated in the first iteration itself.
You could have as well written the above as
li = range(1,lenList)
for i in li:
... your code ...
Changing lenList after li has been created has no effect on li
This problem will become quite a lot easier with one small modification to how your function works: instead of removing similar items from the existing list, create and return a new one with those items omitted.
For the specific case of just removing similarities to the first item, this simplifies down quite a bit, and removes the need to involve Numpy's fancy indexing (which you weren't actually using anyway, because of a missing call to np.array):
import difflib
def similarity(lst):
a = lst[0]
return [a] + \
[x for x in lst[1:] if difflib.SequenceMatcher(None, a, x).ratio() > .9]
From this basis, repeating it for every item in the list can be done recursively - you need to pass the list comprehension at the end back into similarity, and deal with receiving an empty list:
def similarity(lst):
if not lst:
return []
a = lst[0]
return [a] + similarity(
[x for x in lst[1:] if difflib.SequenceMatcher(None, a, x).ratio() > .9])
Also note that importing inside a function, and naming a variable list (shadowing the built-in list) are both practices worth avoiding, since they can make your code harder to follow.

Categories