五种比较流行的排序方法
(1) 冒泡排序
对数组N长度的每一对相邻元素比较,如果比较第一个比第二个大,即交换。
做完一轮交换,那么最大元素会落在数组的最后一个元素。
新的一轮是 除去最后一个元素后的数组N-1进行同样的处理。
【这样的时间复杂度是O(N^2), 实际上是用这种方法经常会超出时间限制】
Python Code
def bubbleSort(arr):
L = len(arr)
for i in range(1, L):
for j in range(0,L-i):
if arr[j] > arr[j+1]:
arr[j], arr[j+1] = arr[j+1], arr[j]
return arr
(2) 选择排序
第一轮:
对于数组N,初始化第一个元素作为最小值,然后对后面的数进行比较,遇到遇到比第一个元素小的值,则更换最小值,继续往后比较,如果还有更小的,则更换最小值,一轮过后,就能找到数组的最小值了,然后跟第一个元素交换。
第二轮:
这个时候,第一个元素是最小值,现在除去第一个元素。对数组N-1重复前面的操作
def selectSort(arr):
##special cases
if arr is None or len(arr)==1: return arr
L = len(arr)
for i in range(L-1):
#每一轮都是寻找最小值
#先set min index
min_index = i
for j in range(i+1, L):
if arr[j] < arr[min_index]:
min_index = j
##循环一轮之后,把最小值换到前面
if min_index != i:
##与最小值交换
arr[i], arr[min_index] = arr[min_index] , arr[i]
return arr
同样时间复杂度是O(logN^2) 一般都会超出时间限制
(3) 快速排序
重点来了 快速排序比较常用。是最实用的排序算法,没有之一,各大语言标准库的排序函数也基本都是基于快排实现的。
把最后一个元素或者是第一个元素作为基准pivot,对前N-1个数进行比较,如果比基准小的 放在左边,大的放在右边
也就是 将未排序的序列排序成 独立的两个部分,其中左边部分序列都比划分值小,右边部分的序列比划分值大,此时划分值的位置已确认,然后再对这两个序列按照同样的方法进行排序。
def partition(arr,low,high):
i = low -1
pivot = arr[high]
for j in range(low,high):
if arr[j]<= pivot:
i +=1
arr[i],arr[j] = arr[j],arr[i]
arr[i+1],arr[high] = arr[high],arr[i+1]
return i + 1
def quickSort(arr,low,high):
if low < high:
mid = partition(arr,0,len(arr)-1)
quickSort(arr,low,mid-1)
quickSort(arr,mid+1,high)
关于快速排序的最好、最坏、平均 时间复杂度分析
快速排序的方法是用第一个数或者是最后一个数作为基准pivot,然后对arr数组遍历。用 i 来代表排序后比pivot 小的最后位置。最后返回i +1 代表划分为两个区间的位置。
(1)现在分析最优情况是:
每次找到的pivot刚好就能把数组arr 平均分成两半。两边的树平衡。
如果数组长度为N,那么根据平衡的递归树的定义则 树的深度D = log(N) + 1
最优情况
第一轮:
第一轮是遍历了所有的数,所以这个时间复杂度是O(N), 下一轮就是对左右两个区间(假设两个区间长度是N/2,因为最优情况)进行搜索
所以,总时间复杂度T(N) = 2* T(N/2) + N
第二轮:
同理针对 T(N/2) 我们继续也是遍历N/2个数 所以 T(N/2) = 2* T(N/4) + N/2
总时间复杂度T(N) = 2* T(N/2) + N = 2*(2* T(N/4) + N/2 ) + N = 4T(N/4) + 2N
T
(
N
)
=
2
2
T
(
N
2
2
)
+
2
N
T(N) = 2^2 T(\frac{N}{2^2}) + 2N
T(N)=22T(22N)+2N
同理针对第三轮也是
T(N) = 2(2( 2T(N/8) + N/4) + N/2) + N = 8T(N/8) + 3N
T
(
N
)
=
2
3
T
(
N
2
3
)
+
3
N
T(N) = 2^3 T(\frac{N}{2^3}) + 3N
T(N)=23T(23N)+3N
第K轮:
T
(
N
)
=
2
K
T
(
N
2
K
)
+
K
N
T(N) = 2^K T(\frac{N}{2^K} )+ KN
T(N)=2KT(2KN)+KN
由于我们知道树的深度是 log(N) ,即K= log(N),带入公式就是
T ( N ) = 2 l o g ( N ) T ( N 2 l o g ( N ) ) + l o g ( N ) N T(N) = 2^{log(N)} T(\frac{N}{2^{log(N)}} )+ {log(N)}N T(N)=2log(N)T(2log(N)N)+log(N)N
由于 2 l o g ( N ) = N 2^{log(N)} = N 2log(N)=N, 所以
T ( N ) = N T ( N N ) + l o g ( N ) N = N T ( 1 ) + N l o g N = N + N l o g N T(N) =NT(\frac{N}{N}) + log(N)N = NT(1) + NlogN =N+ NlogN T(N)=NT(NN)+log(N)N=NT(1)+NlogN=N+NlogN
所以时间复杂度是 O(NlogN)
(2)最坏的情况是 数组是正序或者倒序
当数组是正序或者倒序,每次划分只得到一个比上一次划分少一个记录的子序列,另一个子序列为空,所以组成的递归树是一颗偏斜的树,需要执行n‐1次递归调用。所以有:
N + (N-1) + (N-2) + (N-3) +, + 1 = (N+1)N/2
所以时间复杂度是O(N^2)
【PS:平均时间复杂度可以当作是 一次坏的划分,接着是一次好的划分,轮着来,最后的时间复杂度也是O(nlogn).】
针对空间复杂度是:
快排的空间复杂度是O(logn),因为快排的实现是递归调用的, 而且每次函数调用中只使用了常数的空间,因此空间复杂度等于递归深度O(logn)
(4) 并归排序
先把 nums的所有元素分开 一半,然后一直分,分成一个个单独的[num],再两两排序合并
具体怎么合并呢?用双指针来做 p、q放在两个数组第一位,然后谁小放入ans里面。
归并排序采用分而治之的原理:
(1)将一个序列从中间位置分成两个序列;
(2)再这两个子序列按照第一步继续二分下去;
(3)直到所有子序列的长度都为1,也就是不可以再二分截止。这时候再两两合并成一个有序序列即可。
所以分两步,一步是分开,一步是合并。
(1)递归地分开
def split(nums):
mid = len(nums)//2
left_seq = nums[:mid]
right_seq = nums[mid:]
left = split(left_seq) ##对左区间递归 不断地分开
right = split(right_seq) ##对右区间递归 不断地分开
(2)对任意两个区间 进行合并
def merge(left, right):
ans = []
p,q =0,0
while p<len(left) and q<len(right):
if left[p] < right[q]:
ans.append(left[p])
p+=1
else:
ans.append(right[q])
q+=1
ans += left[p:]
ans += right[q:]
return ans
将两个部分合并在一起,就是并归排序
def merge(left,right):
#合并两个有序数组
ans = []
p,q =0,0
while p < len(left) and q < len(right):
if left[p] < right[q]:
ans.append(left[p])
p+=1
else:
ans.append(right[q])
q+=1
ans += left[p:]
ans += right[q:]
return ans
def mergeSort(nums):
if len(nums)<=1:return nums
#分成两半
mid = len(nums)//2
left_seq = nums[:mid]
right_seq = nums[mid:]
left = mergeSort(left_seq)
right = mergeSort(right_seq)
return merge(left,right)
时间复杂度和空间复杂度
假设nums长度为N
(1)递归部分
递归的第一层是 分成 两个区间 长度分别为N/2
递归的第二层将分成 4个区间长度为N/4
递归的第三层分成8个区间,长度为N/8
…
结束时是
递归的第K层分成(2^K )个区间,长度为N/(2^K)
因为结束的时候,长度是1,所以N/(2^K) = 1
所以,K = logN,即递归了logN层
(2)合并部分
倒着合并,
即在第logN层的时候,每个子区间长度都是1 , 对每相邻的子区间合并。n个数字都会被遍历一次。所以这一层的时间复杂度是 O(N)
…
…
轮到第二层的时候,每个区间的长度为 N/4 总共有4个区间,总共合并两次,n个数字都会被遍历。所以这一层的时间复杂度也是 O(N)
第一层也是一样的道理
所以 合并的时候 每一层都会对n个元素操作,每一层的时间复杂度都是 O(N)。总共有logN层
则总共的时间复杂度是 O(NlogN)
在合并子列时需要申请临时空间,而且空间大小随数列的大小而变化,所以空间复杂度为O(n)
(5) 堆排序
最大堆:最大堆中的最大元素在根结点(堆顶);堆中每个父节点的元素值都大于等于其子结点(如果子节点存在)
最小堆:最小堆中的最小元素出现在根结点(堆顶);堆中每个父节点的元素值都小于等于其子结点(如果子节点存在)
堆排序的过程可以具体分为三步,创建堆,调整堆,堆排序
核心是 先创建一个无序的堆,然后如果父节点为 i,
则它的左节点位置是 2i + 1 ; 右节点是2i + 2;
所以 要比较 把其中数值最大放在父节点上。不断的重复,等到最大的数放在根那里,就可以进行交换,放在末尾端。
不断重复上面的操作。
code:
对父节点以及对应的左右节点:做比较并进行交换把最大值放在父节点上。
def heapify(arr, size, rootIndex):
##子节点
left = 2*rootIndex + 1
right = 2*rootIndex + 2
maxIndex = rootIndex
if left < size and arr[maxIndex] < arr[left]: maxIndex = left
if right < size and arr[maxIndex] < arr[right]: maxIndex = right
if maxIndex != rootIndex:
arr[maxIndex], arr[rootIndex] = arr[rootIndex], arr[maxIndex]
##继续递进
heapify(arr, size, maxIndex)
def heapSort(arr):
##从最后一个节点开始
for i in range(len(arr)-1, -1,-1):
heapify(arr, len(arr), i)
##上面code建好了最大堆
##下面是进行交换 把建立好的最大堆的根节点 也就是最大值 放在列表的末端
##然后重复
for i in range(len(arr)-1,-1,-1):
arr[i], arr[0]=arr[0],arr[i]
#互换之后,堆中就少了一个元素,所以当前堆的个数变了,size变为i
#再建堆
heapify(arr,i,0)
return arr
时间复杂度都是nlogn,但不稳定
【PS:补充一个点 内置函数sort的时间复杂度是nlogn; min和max的时间复杂度是n】
涉及的排序题目
LT215. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
这道是经典的先排序再返回倒数第K个数。
方法一:快速排序
快速排序的特别是partition返回的位置mid,左边有mid个数是比nums[mid]小,右边有L -(mid+1)个数比nums[mid]大。现在要返回倒数第K个数,则如果L-mid=k的话,恰好是返回nums[L-mid],即是返回倒数第K个数。这个便是返回条件。
class Solution:
def partition(self, nums, low, high):
#pivot
pivot = nums[high]
i= low-1
for j in range(low, high):
if nums[j] <= pivot:
i+=1
nums[i], nums[j] = nums[j], nums[i]
nums[i+1], nums[high]=nums[high], nums[i+1]
return i+1
def topK(self, nums, low, high, k):
L = len(nums)
if low < high:
mid = self.partition(nums, low, high)
if L-mid == k: return
elif L-mid > k:
##说明ans在右区间,即[mid+1, high]
self.topK(nums, mid+1, high, k)
else:
self.topK(nums, low, mid-1, k)
def findKthLargest(self, nums: List[int], k: int) -> int:
low = 0
high = len(nums)-1
self.topK(nums, low, high, k)
return nums[-k]
方法二:并归排序
只要记得堆排序的code,最后返回nums[-k]即可
class Solution:
def merge(self, nums1, nums2):
ans = []
p,q =0,0
while p < len(nums1) and q < len(nums2):
if nums1[p] < nums2[q]:
ans.append(nums1[p])
p+=1
else:
ans.append(nums2[q])
q+=1
ans += nums1[p:]
ans += nums2[q:]
return ans
def mergeSort(self, nums):
if len(nums) <= 1: return nums
mid = len(nums)//2
left_seq = nums[:mid]
right_seq = nums[mid:]
left = self.mergeSort(left_seq)
right = self.mergeSort(right_seq)
return self.merge(left, right)
def findKthLargest(self, nums: List[int], k: int) -> int:
ans = self.mergeSort(nums)
return ans[-k]
方法三:堆排序
只要记得堆排序的code,最后返回nums[-k]即可
class Solution:
def heapify(self, nums, size, rootIndex):
#子节点
left = 2*rootIndex + 1
right = 2*rootIndex + 2
#找到最大值的index
maxIndex = rootIndex
if left < size and nums[left] > nums[maxIndex]: maxIndex=left
if right < size and nums[right] > nums[maxIndex]: maxIndex = right
#置换
if maxIndex != rootIndex:
nums[maxIndex], nums[rootIndex] = nums[rootIndex], nums[maxIndex]
#继续递进
self.heapify(nums, size, maxIndex)
def findKthLargest(self, nums: List[int], k: int) -> int:
##堆方法
#从最后一个节点出发
L = len(nums)
for i in range(L-1, -1,-1):
self.heapify(nums, L, i)
for i in range(L-1, -1,-1):
nums[i], nums[0] = nums[0], nums[i]
self.heapify(nums, i, 0)
return nums[-k]
LT4. 寻找两个正序数组的中位数
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的 中位数 。
算法的时间复杂度应该为 O(log (m+n))
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5
输入:nums1 = [], nums2 = [1]
输出:1.00000
这道题咋眼一看,以为使用快速排序、并归排序、堆排序都可以解决。但无论哪种方法,时间复杂度都是(m+n)*log(m+n) 不符合题目要求的时间复杂度。
方法一:
只使用并归排序的merge,时间复杂度是max(len(nums1), len(nums2)),应该比要求的时间复杂度小。
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
p,q = 0,0
ans = []
while p < len(nums1) and q < len(nums2):
if nums1[p] < nums2[q]:
ans.append(nums1[p])
p+=1
else:
ans.append(nums2[q])
q+=1
ans+= nums1[p:]
ans+= nums2[q:]
L = len(ans)
if L%2==1:
return float(ans[L//2])
else:
return float((ans[L//2]+ans[L//2 -1 ])/2)
方法二:【特殊的二分法】
class Solution:
def getKth(self,nums1,nums2,k):
N1 = len(nums1)
N2 = len(nums2)
#p,q的作用是分别记录 两个数组的chi始坐标
p,q = 0,0
while True:
if p == N1: return float(nums2[q+k-1])
if q == N2: return float(nums1[p+k-1])
if k ==1: return float(min(nums1[p], nums2[q]))
index1 = min(p+k//2-1, N1-1)
index2 = min(q+k//2-1, N2-1)
if nums1[index1] <= nums2[index2]:
k = k - index1 + p - 1
p = index1+1
else:
k = k - index2 + q - 1
q = index2 + 1
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
N1 = len(nums1)
N2 = len(nums2)
N = N1 + N2
k = N//2
if N%2==1:
return self.getKth(nums1,nums2,(N+1)//2)
else:
ans = self.getKth(nums1,nums2,k) + self.getKth(nums1,nums2,k+1)
return ans/2