Eliminating nearest elements from a list or range - python

So what I have to do is find the smallest number among those that are in the list, and remove from the list both that number and whichever of its current immediate neighbors is larger. This function should repeat until the largest number in the original list gets eliminated and I have to return the number of times it took to achieve this goal. For example, [1,6,4,2,5,3] would remove element 1 and its current largest neighbor element which is 6, thus reaching the goal in 1 step. My code worked for a list but it did not work when I inputted a range in items. So could anybody help?
def elements(items):
max_value = max(items)
count = 0
while max_value in items:
n = len(items)
right_of_items = 0
left_of_items = 0
min_value = min(items)
min_value_index = items.index(min_value)
if (min_value_index -1 >= 0):
left_of_items = items[min_value_index-1]
if (min_value_index+1 < n):
right_of_items = items[min_value_index+1]
if left_of_items > right_of_items:
items.remove(left_of_items)
items.remove(min_value)
count += 1
else:
items.remove(right_of_items)
items.remove(min_value)
count += 1
return count

The problem with range is that it doesn't support access to a specific item.
So that, you either need to build a list from that range, or use more advanced algorithm which will be able to find the solution in a single pass.
As we won't know the smallest and largest items unless we're analysed the whole list, I guess, there's no such algorithm, that could solve in less than 2 passes, so consider just doing
items = list(items)
at the beginning of your function.
Also I would suggest to not to store the values of a smallest and largest items themselves. Better store their positions. So that you'll be able to access (and remove) neighbours much more effeciently. Like:
def find_largest_neighbour(lst, pos):
if pos <= 0:
return pos + 1
if pos + 1 >= len(lst):
return pos - 1
if lst[pos - 1] <= lst[pos + 1]:
return pos - 1
else:
return pos + 1
def find_min_and_max(lst):
min_pos = max_pos = 0
for i, v in enumerate(lst):
if v < lst[min_pos]:
min_pos = i
if v > lst[max_pos]:
max_pos = i
return min_pos, max_pos
def solve(lst):
number_of_iterations = 0
while True:
number_of_iterations += 1
min_pos, max_pos = find_min_and_max(lst)
max_pos_about = find_largest_neighbour(lst, min_pos)
if max_pos_about == max_pos:
# The largest item found about the smallest, we're done
break
else:
# Getting rid of the smallest item and its largest bro
del lst[min_pos]
del lst[max_pos_about]
# as we used index lookups above, we
# won't search through the list twice
return number_of_iterations

Related

Making the complexity smaller (better)

I have an algorithm that looks for the good pairs in a list of numbers. A good pair is being considered as index i being less than j and arr[i] < arr[j]. It currently has a complexity of O(n^2) but I want to make it O(nlogn) based on divide and conquering. How can I go about doing that?
Here's the algorithm:
def goodPairs(nums):
count = 0
for i in range(0,len(nums)):
for j in range(i+1,len(nums)):
if i < j and nums[i] < nums[j]:
count += 1
j += 1
j += 1
return count
Here's my attempt at making it but it just returns 0:
def goodPairs(arr):
count = 0
if len(arr) > 1:
# Finding the mid of the array
mid = len(arr)//2
# Dividing the array elements
left_side = arr[:mid]
# into 2 halves
right_side = arr[mid:]
# Sorting the first half
goodPairs(left_side)
# Sorting the second half
goodPairs(right_side)
for i in left_side:
for j in right_side:
if i < j:
count += 1
return count
The current previously accepted answer by Fire Assassin doesn't really answer the question, which asks for better complexity. It's still quadratic, and about as fast as a much simpler quadratic solution. Benchmark with 2000 shuffled ints:
387.5 ms original
108.3 ms pythonic
104.6 ms divide_and_conquer_quadratic
4.1 ms divide_and_conquer_nlogn
4.6 ms divide_and_conquer_nlogn_2
Code (Try it online!):
def original(nums):
count = 0
for i in range(0,len(nums)):
for j in range(i+1,len(nums)):
if i < j and nums[i] < nums[j]:
count += 1
j += 1
j += 1
return count
def pythonic(nums):
count = 0
for i, a in enumerate(nums, 1):
for b in nums[i:]:
if a < b:
count += 1
return count
def divide_and_conquer_quadratic(arr):
count = 0
left_count = 0
right_count = 0
if len(arr) > 1:
mid = len(arr) // 2
left_side = arr[:mid]
right_side = arr[mid:]
left_count = divide_and_conquer_quadratic(left_side)
right_count = divide_and_conquer_quadratic(right_side)
for i in left_side:
for j in right_side:
if i < j:
count += 1
return count + left_count + right_count
def divide_and_conquer_nlogn(arr):
mid = len(arr) // 2
if not mid:
return 0
left = arr[:mid]
right = arr[mid:]
count = divide_and_conquer_nlogn(left)
count += divide_and_conquer_nlogn(right)
i = 0
for r in right:
while i < mid and left[i] < r:
i += 1
count += i
arr[:] = left + right
arr.sort() # linear, as Timsort takes advantage of the two sorted runs
return count
def divide_and_conquer_nlogn_2(arr):
mid = len(arr) // 2
if not mid:
return 0
left = arr[:mid]
right = arr[mid:]
count = divide_and_conquer_nlogn_2(left)
count += divide_and_conquer_nlogn_2(right)
i = 0
arr.clear()
append = arr.append
for r in right:
while i < mid and left[i] < r:
append(left[i])
i += 1
append(r)
count += i
arr += left[i:]
return count
from timeit import timeit
from random import shuffle
arr = list(range(2000))
shuffle(arr)
funcs = [
original,
pythonic,
divide_and_conquer_quadratic,
divide_and_conquer_nlogn,
divide_and_conquer_nlogn_2,
]
for func in funcs:
print(func(arr[:]))
for _ in range(3):
print()
for func in funcs:
arr2 = arr[:]
t = timeit(lambda: func(arr2), number=1)
print('%5.1f ms ' % (t * 1e3), func.__name__)
One of the most well-known divide-and-conquer algorithms is merge sort. And merge sort is actually a really good foundation for this algorithm.
The idea is that when comparing two numbers from two different 'partitions', you already have a lot of information about the remaining part of these partitions, as they're sorted in every iteration.
Let's take an example!
Consider the following partitions, which has already been sorted individually and "good pairs" have been counted.
Partition x: [1, 3, 6, 9].
Partition y: [4, 5, 7, 8].
It is important to note that the numbers from partition x is located further to the left in the original list than partition y. In particular, for every element in x, it's corresponding index i must be smaller than some index j for every element in y.
We will start of by comparing 1 and 4. Obviously 1 is smaller than 4. But since 4 is the smallest element in partition y, 1 must also be smaller than the rest of the elements in y. Consequently, we can conclude that there is 4 additional good pairs, since the index of 1 is also smaller than the index of the remaining elements of y.
The exact same thing happens with 3, and we can add 4 new good pairs to the sum.
For 6 we will conclude that there is two new good pairs. The comparison between 6 and 4 did not yield a good pair and likewise for 6 and 5.
You might now notice how these additional good pairs would be counted? Basically if the element from x is less than the element from y, add the number of elements remaining in y to the sum. Rince and repeat.
Since merge sort is an O(n log n) algorithm, and the additional work in this algorithm is constant, we can conclude that this algorithm is also an O(n log n) algorithm.
I will leave the actual programming as an exercise for you.
#niklasaa has added an explanation for the merge sort analogy, but your implementation still has an issue.
You are partitioning the array and calculating the result for either half, but
You haven't actually sorted either half. So when you're comparing their elements, your two pointer approach isn't correct.
You haven't used their results in the final computation. That's why you're getting an incorrect answer.
For point #1, you should look at merge sort, especially the merge() function. That logic is what will give you the correct pair count without having O(N^2) iteration.
For point #2, store the result for either half first:
# Sorting the first half
leftCount = goodPairs(left_side)
# Sorting the second half
rightCount = goodPairs(right_side)
While returning the final count, add these two results as well.
return count + leftCount + rightCount
Like #Abhinav Mathur stated, you have most of the code down, your problem is with these lines:
# Sorting the first half
goodPairs(left_side)
# Sorting the second half
goodPairs(right_side)
You want to store these in variables that should be declared before the if statement. Here's an updated version of your code:
def goodPairs(arr):
count = 0
left_count = 0
right_count = 0
if len(arr) > 1:
mid = len(arr) // 2
left_side = arr[:mid]
right_side = arr[mid:]
left_count = goodPairs(left_side)
right_count = goodPairs(right_side)
for i in left_side:
for j in right_side:
if i < j:
count += 1
return count + left_count + right_count
Recursion can be difficult at times, look into the idea of merge sort and quick sort to get better ideas on how the divide and conquer algorithms work.

Reversing the order of a list without using inbuilt functions in Python

I am new to python and in trying to get better I purchased some textbooks. One problem I found in a textbook has me really confused as I feel the solution is obvious. I am given a insertion sort code that orders a list in ascending order. The problem asks to reverse the order of the list (so it is descending) without using in built functions and instead altering the code. Here is the code:
sequence = [5,-2,0,6,10,-5,1]
def myInsertionSort(sequence):
for i in range (1, len(sequence)):
item = sequence[i]
j = i
while j > 0 and sequence[j-1] > item:
sequence[j] = sequence[j-1]
j -= 1
sequence[j] = item
return sequence
myInsertionSort(sequence)
I added an example sequence and the output here is that it orders the sequence in ascending order. I realised the key here was to understand the code, so I used pythontutor.com to visualise and understand each step, and even though I now feel I understand it every edit I make to the code results in an error or un-ordered list.
Consider this slight generalization of your code; I've just refactored the comparison of sequence[j-1] and item to a separate function.
def out_of_order(a, b):
return a > b
def myInsertionSort(sequence):
for i in range (1, len(sequence)):
item = sequence[i]
j = i
while j > 0 and out_of_order(sequence[j-1], item):
sequence[j] = sequence[j-1]
j -= 1
sequence[j] = item
return sequence
The while loop swaps two items if they are considered out of order. Now think about what happens if you change what it means for two items to be out of order by modifying the definition of out_of_order.
Your book example is not very pythonic.
This is called insertion sort. It may help to see how it works with an animation. Take a look at this website (make sure to select insertion [INS] sort): https://visualgo.net/bn/sorting
That be said keep a close eye on the > in the while loop statement sequence[j-1] > item. That is my hint.
sequence = [5,-2,0,6,10,-5,1]
def myInsertionSort(sequence):
for i in range (1, len(sequence)):
item = sequence[i]
j = i
while j > 0 and sequence[j-1] < item:
sequence[j] = sequence[j-1]
j -= 1
sequence[j] = item
return sequence
myInsertionSort(sequence)
If it makes it easier to read, here is there example written a little differently:
sequence = [5,-2,0,6,10,-5,1]
def myInsertionSort(sequence):
for j in range (1, len(sequence)):
while j > 0 and sequence[j-1] > sequence[j]:
sequence[j], sequence[j-1] = sequence[j-1], sequence[j] # Flip the elements
j -= 1 # Move down one index
return sequence
print(myInsertionSort(sequence))
A super easy way is simply to return sequence[::-1] and it will give you an ascending ordered list. I don't consider list reverse technique as in-built-function.
return sequence[::-1]
But if you think that it counts, here is the alternative code:
sequence = [5,-2,0,6,10,-5,1]
def myInsertionSort(sequence):
for i in range (1, len(sequence)):
item = sequence[i] # for example, when i is 0, item = 5
j = i # now j = i = 1
while j > 0 and sequence[j-1] < item:
# our goal is to push smaller numbers towards the end of the list
# e.g. when i = 1, j = 1 sequence[j-1] = sequence[0] = 5, so sequence[0] > sequence[1], we want to keep their position, now move on
#. when i = 1, j = 2, sequence[j-1] = sequence[1] = -2, so sequence[1] < sequence[2], we must switch them
sequence[j] = sequence[j-1] # sequence[1] is reassigned the current value of sequence[2] because the latter is bigger
j -= 1 # now we also must put the smaller value in the current sequence[2] position
sequence[j] = item # sequence[2] = item (item is the sequence[1])
return sequence
myInsertionSort(sequence)

Minimum count to sort an array in Python by sending the element to the end

Here is the explanation of what I'm trying to say:-
Input:- 5 1 3 2 7
Output:- 3
Explanation:
In first move, we move 3 to the end. Our list becomes 5,1,2,7,3
In second move, we move 5 to the end. Our list becomes 1,2,7,3,5
In third move, we move 7 to the end. Our final list = 1,2,3,5,7
So, total moves are:- 3.
Here is what I tried to do, but failed.
a = [int(i) for i in input().split()]
count = 0
n = 0
while (n < len(a) - 1):
for i in range(0,n+1):
while (a[i] > a[i + 1]):
temp = a[i]
a.pop(i)
a.append(temp)
count += 1
n += 1
print(count, end='')
I'd like to request your assistance in helping in solving this question.
jdehesa's answer is basically right, but not optimal for cases, when there is more element of same value. Maybe more complex solution?
def min_moves(a):
c = 0
while(1):
tmp = None
for i in range(0, len(a)):
if a[i] != min(a[i:]) and (tmp is None or a[i] < a[tmp]):
tmp = i
if tmp is None:
return c
else:
a.append(a.pop(tmp))
c += 1
Edit:
Or if you don't need ordered list, there's much more easier solution just to count items that are out of order for the reason from jdehesa's solution :-D
def min_moves(a):
c = 0
for i in range(0, len(a)):
if a[i] != min(a[i:]):
c += 1
return c
Edit 2:
Or if you like jdehesa's answer more, small fix is to reduce lst to set, so it will get smallest index
sorted_index = {elem: i for i, elem in enumerate(sorted(set(lst)))}
I cannot comment yet.
I don't know if it can be done better, but I think the following algorithm gives the right answer:
def num_move_end_sort(lst):
# dict that maps each list element to its index in the sorted list
sorted_index = {elem: i for i, elem in enumerate(sorted(lst))}
moves = 0
for idx, elem in enumerate(lst):
if idx != sorted_index[elem] + moves:
moves += 1
return moves
print(num_move_end_sort([5, 1, 3, 2, 7]))
# 3
The idea is as follows. Each element of the list would have to be moved to the end at most once (it should be easy to see that a solution that moves the same element to the end more than once can be simplified). So each element in the list may or may not need to be moved once to the end. If an element does not need to be moved is because it ended up in the right position after all the moves. So, if an element is currently at position i and should end up in position j, then the element will not need to be moved if the number of previous elements that need to be moved, n, satisfies j == i + n (because, after those n moves, the element will indeed be at position j).
So in order to compute that, I sorted the list and took the indices of each element in the sorted list. Then you just count the number of elements that are not in the right position.
Note this algorithm does not tell you the actual sequence of steps you would need to take (the order in which the elements would have to be moved), only the count. The complexity is O(n·log(n)) (due to the sorting).
I think you can simplify your problem,
Counting elements that need to be pushed at the end is equivalent to counting the length of the elements that are not in sorted order.
l = [5, 1, 3, 2, 7]
sorted_l = sorted(l)
current_element = sorted_l[0]
current_index = 0
ans = 0
for element in l:
if current_element == element:
current_index += 1
if current_index < len(l):
current_element = sorted_l[current_index]
else:
ans += 1
print(ans)
Here the answer is 3

Reference Link List Length Python?

EDIT: The terminology I was looking for was is called Cycle Detection. Thanks to #dhke for referring that in the comments.
I'm trying to figure out a better way to process a list of indexes and what it's length is if a list has a loop in its reference. I have a function that works but it passes the next index value and counter. I've been trying to figure out a way to do it by just passing the list into the function. It always starts as index 0.
Given a list, each node in the list references the index of some other node. I'm trying to get the length of the linked list not the number of nodes in the list.
# This list would have a length of 4, index 0->1->3->6->0
four_links_list = [1,3,4,6,0,4,0]
two_links_list = [3,2,1,0]
def my_ideal_func(list):
# Some better way to iterate over the list and count
def my_func(list, index, counter):
# We're just starting out
if index == 0 and counter == 0:
counter += 1
return my_func(list, list[index], counter)
# Keep going through the list as long as we're not looping back around
elif index != 0:
counter += 1
return my_func(list, list[index], counter)
# Stop once we hit a node with an index reference of 0
else:
return counter
If you don't want extra data structures:
def tortoise_and_hare(l):
tort = 0
hare = 0
count = 0
while tort != hare or count == 0:
count += 1
if l[tort] == 0:
return count
tort = l[tort]
hare = l[hare]
hare = l[hare]
return -1
>>> tortoise_and_hare([1,3,4,6,0,4,0])
4
>>> tortoise_and_hare([3,2,1,0])
2
>>> tortoise_and_hare([1,2,3,1,2,1,2,1])
-1
You can use a set to keep track of all nodes you've visited (sets have very fast membership tests). And there is absolutely no need for recursion here, a loop will do nicely:
def my_ideal_func(list):
visited_nodes= set()
index= 0
length= 0
while True:
node= list[index]
if node in visited_nodes:
return length
visited_nodes.add(node)
length+= 1
index= list[index]
There's no need for recursion:
def link_len(l):
cnt, idx = 0, 0
while not cnt or idx:
cnt = cnt + 1
idx = l[idx]
return cnt
This assumes the list loops back to 0.

Finding the "centered average" of a list

"Return the "centered" average of a list of integers, which we'll say is the mean average of the values, except ignoring the largest and smallest values in the list. If there are multiple copies of the smallest value, ignore just one copy, and likewise for the largest value. Use integer division to produce the final average. You may assume that the list is length 3 or more."
This is a problem I have from my homework assignment and I am stumped at how to find the the largest/smallest numbers and cut them out of the list. Here is what I have so far. and It works for 10/14 the scenarios that I have to pass.. I think it is just because it grabs the median
def centered_average(nums):
x = 0
for i in range(len(nums)):
x = i + 0
y = x + 1
if y%2 == 0:
return (nums[y/2] + nums[(y/2)+1]) / 2
else:
return nums[y/2]
Sorting the array is certainly terser code, here's an alternative with a manual loop
max_value = nums[0]
min_value = nums[0]
sum = 0
for x in nums:
max_value = max(max_value, x)
min_value = min(min_value, x)
sum += x
return (sum - max_value - min_value) / (len(nums) - 2)
This just adds everything in and removes the max and min at the end.
If the list isn't too long, it shouldn't be too computationally expensive to sort the list:
sorted(nums)
Then you can create a new list without the first and last entries, which will be the smallest and largest values:
new_nums = sorted(nums)[1:-1] # from index 1 to the next-to-last entry
Before i start i know there are easier ways mentioned in the other answers using the function sort, yes that is true but i believe your teacher must have iven you this to able to master loops and use them logically.
First pick your first number and assign it to high and low, don't worry it will make sense afterwards.
def centered average(nums):
high = nums[0]
small = nums[0]
Here is were the magic happens, you loop through your list and if the number your on in the loop is larger then the previous ones then you can replace the variable high with it, let me demonstrate.
for count in nums:
if count > high:
high = count
if count < low:
low = count
Now you have the low and the high all you do is add the values of the loop together minus the high and the low (as you said you do not need them).Then divide that answer by len of nums.
for count in nums:
sum = count + sum
sum = sum - (high + low)
return sum
New here. I like to check my solutions with solutions found on the internet and did not see my code here yet (hence the post). I found this challenge on https://codingbat.com/prob/p126968. And here is my solution:
** This is done in Python 3.9.1.
First the min and max are popped from the list with the index method. After it's just a simple avg calculation.
def centered_average(nums):
nums.pop(nums.index(max(nums)))
nums.pop(nums.index(min(nums)))
return sum(nums)/len(nums)
If I understand the question, this should work:
def centered_average(nums):
trim = sorted(nums)[1:-1]
return sum(trim) / len(trim)
def centered_average(nums):
nums = sorted(nums)
for i in range(len(nums)):
if len(nums)%2 != 0:
return nums[len(nums)/2]
else:
return ((nums[len(nums)/2] + nums[len(nums)/2 - 1]) / 2)
This is a very sub standard solution to the problem. This code is a bad code that does not take into account any consideration for complexity and space. But I think the thought process to be followed is similar to the steps in the code. This then can be refined.
def centered_average(nums):
#Find max and min value from the original list
max_value = max(nums)
min_value = min(nums)
#counters for counting the number of duplicates of max and min values.
mx = 0
mn = 0
sum = 0
#New list to hold items on which we can calculate the avg
new_nums = []
#Find duplicates of max and min values
for num in nums:
if num == max_value:
mx += 1
if num == min_value:
mn += 1
#Append max and min values only once in the new list
if mx > 1:
new_nums.append(max_value)
if mn > 1:
new_nums.append(min_value)
#Append all other numbers in the original to new list
for num in nums:
if num != max_value and num != min_value:
new_nums.append(num)
#Calculate the sum of all items in the list
for new in new_nums:
sum += new
#Calculate the average value.
avg = sum/len(new_nums)
return avg
def centered_average(nums):
min1=nums[0]
max1=nums[0]
for item in nums:
if item > max1:
max1 = item
if item < min1:
min1 = item
sum1=(sum(nums)-(min1+max1))/(len(nums)-2)
return sum1
simple solution
def centered_average(nums):
b=nums
ma=max(b)
mi=min(b)
l=(len(b)-2)
s=sum(b)-(ma+mi)
av=int(s/l)
return av
use sum function to sum the array
max and min functions to get the biggest and smallest number
def centered_average(nums):
return (sum(nums) - max(nums) - min(nums)) / (len(nums) - 2)
def centered_average(nums):
sorted_list = sorted(nums)
return sum(sorted_list[1:-1])/(len(nums)-2)
This will get the job done.
Python 3 Solution using list.index, list.pop, min and max functions.
def solution(input):
average = 0
minimum = min(input)
maximum = max(input)
input.pop(input.index(minimum))
input.pop(input.index(maximum))
average = round(sum(input) / len(input))
return average
def centered_average(nums):
nums.remove((min(nums)))
nums.remove((max(nums)))
new_nums=nums
count = 0
for i in range(len(new_nums)):
count+=1
ans=sum(new_nums)//count
return ans
def centered_average(nums):
maximums = []
minimums = []
sum_of_numbers = 0
length =len(nums) + (len(minimums)-1) + (len(maximums)-1)
for i in nums:
if i == max(nums):
maximums.append(i)
elif i == min(nums):
minimums.append(i)
else:
sum_of_numbers += i
if len(maximums)>=2 or len(minimums)>=2:
sum_of_numbers = sum_of_numbers + (max(nums)*(len(maximums)-1))(min(nums)*(len(minimums)-1))
return sum_of_numbers / length

Categories

Resources