排序
冒泡排序:依次比较相邻元素,进行len-1次冒泡,第K次冒泡将倒数第k个元素排好序
for i in range(len(s)):
for j in range(len(s)-i):
if s[j]>s[j+1]:
s[j],s[j+1]=s[j+1],s[j]
快速排序: 递归加分治 215
不稳定排序,中间交换过程会打乱顺序
时间复杂度:最差O(N^ 2),平均O(N^logN)
取中间值作为基准,数列从第一个值开始比较,小于基准值放入左边分区,大于基准值放入右边分区
然后依次对左右两个分区进行再分区,直到最后只有一个元素
分解完成再一层一层返回,返回规则是:左边分区+基准值+右边分区
def quick_sort(nums):
if len(nums)<2:
return nums
left,right=[],[]
mid=nums[len(nums)//2]
# 从原始数组中移除基准值
nums.remove(mid)
for num in nums:
if num<mid:
left.append(num)
else:
right.append(num)
return quick_sort(left)+[mid]+quick_sort(right)
算法导论中的快排:
快排实现原地排序,期望时间复杂度只有O(nlogn)。其中n是数组的长度。在最不差的情况下,快排的时间复杂度会退化到O(n2). 但是这种情况出现的概率很小
1.选取关键字,2.将关键字放在正确的位置上:关键字左边的数都比它小,右边的数都比它大。这样,这个关键字在接下来的排序中都不会改变位置。
i是一个关键的位置:i是比值关键字A[r]小的序列的末尾位置。i+1则是比A[r]大的数的区间的最左边的位置。i从整个区间最左端p-1开始不断增加。在整个过程结束之后,A[r]会和A[i+1]交换,就是和比A[r]大的序列的最左边的数交换,来达到A[r]的正确位置。(h步中的8和4交换)
A[j]小于等于x时,要讲A[j]交换到前面,因为【i+1,j】是比关键字大的区间,所以i的值增加,同时交换此时i和j的值。但是当A[j]的值大于x时,这个位置上的数不应该在x的左边区间,因此i不动,j继续右移。[i+1,j]就是比关键字大的数的区间。[p,i]是比关键字x小的区间
因为选取的关键字A[r]在区间的最右边,这个位置应该放比关键字大的数。因此,最后一步是将区间[i+1,j]的最左边的位置上的数值和关键字交换。
快排保证了每次排序关键字的位置一定正确,关键字左侧数据小于关键字,但顺序未知,关键字右侧数据大于关键字,顺序未知
下面代码是是升序有序,就是右边的数大于等于左边的数,这里由array[j] <= x
决定。如果想要非增序排序,则i向右移动的条件是array[j] >= x
。
快排可以跳过一些元素的比较,在对一个区间进行划分之后,形成了两个区间。前一个区间里的数是没有必要和后面区间里面的数比较的。划分完成之后,不仅给关键字找到了最终的位置,而且还隔离了比较域。
https://blog.csdn.net/uestc_my/article/details/45600499?utm_source=copy
def quick_sort(array, l, r):
if l < r:
#一趟排序后,q左边的值都比q小,q右边的值都比q大
q = partition(array, l, r)
quick_sort(array, l, q - 1)
quick_sort(array, q + 1, r)
#原地排序,不需要返回值
def partition(array, l, r):
#选择区间尾作为基准
x = array[r]
i = l - 1
for j in range(l, r):
if array[j] <= x:
#当J位置值比基准值小时,i,j一起移动
i += 1
array[i], array[j] = array[j], array[i]
array[i + 1], array[r] = array[r], array[i+1]
return i + 1
归并排序:
O(N^logN)
首先归并排序使用了二分法,归根到底的思想还是分而治之。拿到一个长数组,将其不停的分为左边和右边两份,然后以此递归分下去。然后再将她们按照两个有序数组的样子合并起来
将其分割成只剩单一元素,在进行比较合并
#合并
def merge(l,r):
i,j=0,0
c=[]
while i<len(l) and j<len(r):
if l[i]<r[j]:
c.append(l[i])
i+=1
else:
c.append(r[j])
j+=1
if i<len(l):
c.extend(l[i:])
if j<len(r):
c.extend(r[j:])
return c
#分割
def merge_sort(nums):
if len(nums)<2:
return nums
mid=len(nums)//2
left=merge_sort(nums[:mid])
right=merge_sort(nums[mid:])
return merge(left,right)
桶排序:
分桶,合并
1.基数排序
2.次数排序
统计每一个数字出现的次数,输出次数次即可
桶排序也叫计数排序,简单来说,就是将数据集里面所有元素按顺序列举出来,然后统计元素出现的次数。最后按顺序输出数据集里面的元素。
比较占用内存,如果数组范围较大,那么需要申请一个很大的空间,但是速度很快
def bucketSort(nums):
# 选择一个最大的数
max_num = max(nums)
# 创建一个元素全是0的列表, 当做桶
bucket = [0]*(max_num+1)
# 把所有元素放入桶中, 即把对应元素个数加一
for i in nums:
bucket[i] += 1
# 存储排序好的元素
sort_nums = []
# 取出桶中的元素
for j in range(len(bucket)):
if bucket[j] != 0:
for y in range(bucket[j]):
sort_nums.append(j)
return sort_nums
912. 排序数组
快速排序:
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
if len(nums)<2:
return nums
left,right=[],[]
mid=nums[len(nums)//2]
nums.remove(mid)
for num in nums:
if num<=mid:
left.append(num)
else:
right.append(num)
return self.sortArray(left)+[mid]+self.sortArray(right)
归并排序:
时间复杂度:O(N^logN)
首先归并排序使用了二分法,归根到底的思想还是分而治之。拿到一个长数组,将其不停的分为左边和右边两份,然后以此递归分下去。然后再将她们按照两个有序数组的样子合并起来
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
def merge(l,r):
i,j=0,0
c=[]
while i<len(l) and j<len(r):
if l[i]<r[j]:
c.append(l[i])
i+=1
else:
c.append(r[j])
j+=1
if i<len(l):
c.extend(l[i:])
if j<len(r):
c.extend(r[j:])
return c
def merge_sort(nums):
if len(nums)<2:
return nums
mid=len(nums)//2
left=merge_sort(nums[:mid])
right=merge_sort(nums[mid:])
return merge(left,right)
return merge_sort(nums)
桶排序
164. 最大间距
本题不需要真正将所有元素严格排序,只需要求出最大的间隔即可,考虑桶排序。将元素分配到不同的桶中,桶的大小一样,桶内的元素无序,且桶有序
n个元素的数组有n-1个间隔,所有元素间隔一致时的间隔是最小的最大间隔(因为缩小任意一个间隔,一定有另一个间隔增大),所以按平均分配间隔来建立桶,平均间隔为margin=max(1, (max-min)//(n-1)),
桶的个数b=ceil((max-min)/margin) ceil:向上取整
桶间间隔由后面桶的最小值减去前面桶的最大值得到,跳过没有元素的桶。
如果区间里面的数字都是等间隔排列的,那最大的间隔值一定是最小的,这种情况下,任何一个数值不论增大还是减小,都只可能会造成最大间隔变大,不可能让情况变更好,把这个等间隔作为桶的大小(margin=max(1, (max-min)//(n-1))),最大间隔一定比这个桶大小要大,所以分在一个桶里面的数值之间差值不可能是最大间隔,最大间隔只可能产生在相邻的桶之间,一定值前一个桶的最大值和后一个桶的最小值的差值,所以只需要维护每一个桶里面的最大值和最小值,最后把相邻的桶遍历一遍即可,时间复杂度是线性的
通过( num-minnum)//margin判断属于哪个桶
所有元素遍历完后,比较k-1个相邻桶间距的最大值即可
对于n个元素的数组,有n+1个桶,那么至少会有一个空桶,那么最大间距一定是最大桶间间距(跳过空桶)
class Solution:
def maximumGap(self, nums: List[int]) -> int:
if len(nums)<2:
return 0
maxm,minm=max(nums),min(nums)
n=len(nums)
#平均间隔,用于计算每个数据属于哪个桶
margin=max(1,(maxm-minm)//n-1)
#桶的个数
b=(maxm-minm)//margin+1
# 保存桶内最小值和最大值
bucke=[[float('inf'),float('-inf')]for i in range(b)]
for num in nums:
loc=(num-minm)//margin
#维护桶内最大最小值
bucke[loc][0]=min(num,bucke[loc][0])
bucke[loc][1]=max(num,bucke[loc][1])
res=float('-inf')
premin=minm
for x,y in bucke:
#跨过空桶
if x==float('inf'):
continue
#后一个桶的最小值减去前一个桶的最大值
res=max(res,x-premin)
premin=y
res=max(res,maxm-premin)
return res
220 存在重复元素 III
将元素按顺序分配到桶内,分配规则是按照 nums[i] // (t + 1),桶的大小是t,同一个桶内元素最大差值是t,桶间元素最大差值是2*t+1,
0-t t+1-2t+1 2t+2-3t+2 …
例如 t=0,说明一个元素一个桶
如果命中同一个桶,则直接返回True
如果命中相邻桶,判断差值是否<=t
否则返回False
由于题目有索引相差k的要求,因此要维护一个大小为k的窗口,定期清除桶中过期的数字
哈希表的key作为桶的编号,Value是元素
class Solution:
def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:
if t<0:
return False
#将元素分到桶内,桶的大小为t,则桶内数据最大差值是t
bucket=defaultdict(list)
for i in range(len(nums)):
loc=nums[i]//(t+1)
#说明桶内已经有数据,符合条件索引差小于等于k,元素差小于等于t,返回True
if loc in bucket:
return True
#桶内没有元素,则比较相邻桶之间数据差是否满足要求
if loc-1 in bucket and abs(bucket[loc-1]-nums[i])<=t:
return True
if loc+1 in bucket and abs(bucket[loc+1]-nums[i])<=t:
return True
bucket[loc]=nums[i]
#定期删除索引范围差超过K的元素,为了下一次循环准备桶,保证bucket中最多只有k个桶(key)
if i>=k:
bucket.pop(nums[i-k]//(t+1))
return False
451. 根据字符出现频率排序
按照元素出现次数分桶,出现次数相同的放到一个桶里,从大编号(出现次数多的)桶到小编号桶输出
class Solution:
def frequencySort(self, s: str) -> str:
n=len(s)
res=[]
bucket=[[] for i in range(n+1)]
map=defaultdict(int)
for i in s:
map[i]+=1
# 按照字符出现次数分桶,桶内元素是字符
for i in map:
bucket[map[i]].extend(i*map[i])
#按照桶由大到小输出
for m in bucket[::-1]:
if m:
res.extend(m)
return ''.join(res)
归并排序
能够看到非常明显效果的阶段性排序结果
面试题51. 数组中的逆序对
在归并算法中,左右子序列都是有序序列,因此,当归并时,出现左子序列的某一元素
大于右子序列的某一元素时,说明左子序列剩下的所有元素都比右子序列的该元素大,因此
在归并中就可得到所有跨界逆序对数,即 len(left)-i ,只有当右子序列指针需要移动时才计算(说明次数右子序列j的元素小于左子序列i)
右边元素归并回去的时候,将左边元素还没归并回去的数目加到计数器变量上
计算逆序对个数发生在合并时候
class Solution:
def reversePairs(self, nums: List[int]) -> int:
if len(nums)<=1:
return 0
def merge(left,right):
i,j=0,0
m,n=len(left),len(right)
tmp=[]
while i<len(left) and j<len(right):
if left[i]<=right[j]:
tmp.append(left[i])
i+=1
else:
#当右边小于左边元素时,更新逆序对数量
self.count+=(m-i)
tmp.append(right[j])
j+=1
if i<m:
tmp.extend(left[i:])
if j<n:
tmp.extend(right[j:])
return tmp
def merge_sort(nums):
if len(nums)<2:
return nums
mid=len(nums)//2
left=merge_sort(nums[:mid])
right=merge_sort(nums[mid:])
return merge(left,right)
self.count=0
merge_sort(nums)
return self.count
方法同理,只是每次修改Nums,虽然速度并没有起来,
class Solution:
def reversePairs(self, nums: List[int]) -> int:
self.cnt = 0
def merge(nums, start, mid, end, temp):
i, j = start, mid + 1
while i <= mid and j <= end:
if nums[i] <= nums[j]:
temp.append(nums[i])
i += 1
else:
self.cnt += mid - i + 1
temp.append(nums[j])
j += 1
while i <= mid:
temp.append(nums[i])
i += 1
while j <= end:
temp.append(nums[j])
j += 1
for i in range(len(temp)):
nums[start + i] = temp[i]
temp.clear()
def mergeSort(nums, start, end, temp):
if start >= end: return
mid = (start + end) >> 1
mergeSort(nums, start, mid, temp)
mergeSort(nums, mid + 1, end, temp)
merge(nums, start, mid, end, temp)
mergeSort(nums, 0, len(nums) - 1, [])
return self.cnt
315. 计算右侧小于当前元素的个数 类似上题
降序排序,当左边元素i大于右边元素j时,说明i大于剩下的len(right)-j个元素
为了保证排序过程中索引变换导致数据相加错误,用索引数组排序,不用原始数组
class Solution:
def countSmaller(self, nums: List[int]) -> List[int]:
self.count=[0 for i in range(len(nums))]
def merge(left,right):
i,j=0,0
m,n=len(left),len(right)
tmp=[]
while i<m and j<n:
if nums[left[i]]>nums[right[j]]:
self.count[left[i]]+=n-j
tmp.append(left[i])
i+=1
else:
tmp.append(right[j])
j+=1
if i<m:
tmp.extend(left[i:])
if j<n:
tmp.extend(right[j:])
return tmp
def merge_sort(nums):
if len(nums)<2:
return nums
mid=(len(nums))//2
left=merge_sort(nums[:mid])
right=merge_sort(nums[mid:])
return merge(left,right)
list=[i for i in range(len(nums))]
merge_sort(list)
return self.count
327. 区间和的个数
前缀和+归并排序
在排序过程中,每次将left和right列表合并前,为left中的每一个元素确定right中符合条件的值的范围。也就是说在right中设两个指针lo和up,其中lo指向right中第一个使right[lo] - left[i] >= lower的元素,而up指向right中最后一个使right[up] - left[i] <= upper的元素,将up-lo加到最后的结果上即可
由于可能存在重叠的数据,所以对于下界我们找左插入点,上界我们找右插入点。
归并排序的前提是合并时候,两边都是有序的,所以可以根据上下界求区间
代码根据这个写的
https://leetcode-cn.com/problems/count-of-range-sum/solution/327qu-jian-he-de-ge-shu-ti-jie-zong-he-by-xu-yuan-/
class Solution:
def countRangeSum(self, nums: List[int], lower: int, upper: int) -> int:
def merge_sort(nums):
if len(nums)<2:
return nums
mid=len(nums)//2
left=merge_sort(nums[:mid])
right=merge_sort(nums[mid:])
return merge(left,right)
def merge(left,right):
i,j,lo,up=0,0,0,0
m,n=len(left),len(right)
#固定左边i,求右边j
for le in range(m):
while lo<n and right[lo]-left[le]<lower:
lo+=1
while up<n and right[up]-left[le]<=upper:
up+=1
self.count+=up-lo
tmp=[]
while i<m and j<n:
if left[i]<=right[j]:
tmp.append(left[i])
i+=1
else:
tmp.append(right[j])
j+=1
if i<m:
tmp.extend(left[i:])
if j<n:
tmp.extend(right[j:])
return tmp
self.count=0
#求前缀和
s=[0]*(len(nums)+1)
for i in range(1,len(nums)+1):
s[i]=s[i-1]+nums[i-1]
merge_sort(s)
return self.count
493. 翻转对
同找逆序对,用归并排序,在合并的时候进行翻转对的判断和数量相加
时间复杂度:O(Nlog(n))
空间复杂度:O(N)
class Solution:
def reversePairs(self, nums: List[int]) -> int:
self.count=0
def merge(nums):
if len(nums)<2:
return nums
mid=len(nums)//2
left=merge(nums[:mid])
right=merge(nums[mid:])
return merge_sort(left,right)
def merge_sort(left,right):
i,j=0,0
tmp=[]
m,n=len(left),len(right)
r=0
for l in range(m):
#固定i,右移符合条件的j,即找到最右边的r,符合left[l]>2*right[r]
while r<n and left[l]>2*right[r]:
r+=1
self.count+=r
while i<m and j<n:
if left[i]<right[j]:
tmp.append(left[i])
i+=1
else:
tmp.append(right[j])
j+=1
if i<m:
tmp.extend(left[i:])
if j<n:
tmp.extend(right[j:])
return tmp
merge(nums)
return self.count
148 排序链表:
nlog(n):要求归并排序
分割:找到链表中点处断开,使用快慢指针,奇数个节点找到中点,偶数个节点找到中点左边的节点,找到中点后,设置右端头结点,同时断开左右两段链表,
递归分割时,输入当前链表左端点 head 和中心节点 slow 的下一个节点 tmp(因为链表是从 slow 切断的)。
cut 递归终止条件: 当head.next == None时,说明只有一个节点了,直接返回此节点
合并:谁小接谁
返回:头结点
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def sortList(self, head: ListNode) -> ListNode:
if not head or not head.next:
return head
#找中点
fast,slow=head.next,head
while fast and fast.next:
fast,slow=fast.next.next,slow.next
mid=slow.next
slow.next=None
left=self.sortList(head)
right=self.sortList(mid)
tmp=dummy=ListNode(0)
while left and right:
if left.val<right.val:
tmp.next=left
left=left.next
else:
tmp.next=right
right=right.next
tmp=tmp.next
tmp.next=left if left else right
return dummy.next
215 数组中第K个最大元素
O(nlogn) 利用快速排序,每次判断基准值位置是不是第k个,利用非递增顺序排列,不是的话改变左右区间 边界,每次都缩小第k大数所在区间的边界
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
#快速排序 倒着排
def partition(nums,l,r):
pivort=nums[r]
i=l-1
for j in range(l,r):
# 非递增排序,将大的值换到前面俩
if nums[j]>=pivort:
i+=1
nums[i],nums[j]=nums[j],nums[i]
#选取的关键字在区间的最右边,这个位置应该放比关键字小的数字,因此最后交换i+1和r位置的值
nums[i+1],nums[r]=nums[r],nums[i+1]
return i+1
l,r=0,len(nums)-1
while True:
p=partition(nums,l,r)
#如果关键字位置就是第k个元素,因为是降序排序,所以是第K大元素
if p+1==k:
return nums[p]
# 说明p前面大的数不足k个,因为第k大元素去P的右侧区间找
elif p+1<k:
l=p+1
else:
r=p-1
219. 存在重复元素 II
class Solution:
def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool:
if k==0:
return False
#滑动窗口大小为K
hash=defaultdict(int)
for r in range(len(nums)):
if nums[r] in hash:
return True
if r>=k:
hash.pop(nums[r-k])
hash[nums[r]]=r
return False
217. 存在重复元素
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
hash=set()
for i in range(len(nums)):
if nums[i] in hash:
return True
hash.add(nums[i])
return False