本小节介绍三个基本排序算法,基于比较且运行最慢的算法,包括冒泡排序、选择排序以及插入排序这三种算法。这三种算法的共同特点是慢,但也是后面优化的基础,因此需要掌握这三种算法原理。
第 i ( i = 1 , 2 , . . . ) i(i=1,2,...) i(i=1,2,...)趟排序时从序列中前 n − i + 1 n - i + 1 n−i+1 个元素的第 1 1 1个元素开始,相邻两个元素进行比较,若前者大于后者,两者交换位置,否则不交换。
冒泡排序法是通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面,就像水底的气泡一样向上冒,故称这种排序方法为冒泡排序法。
def bubbleSort(arr):
for i in range(len(arr)):
for j in range(len(arr)-i-1):
if arr[j]>arr[j+1]:
arr[j],arr[j+1]=arr[j+1],arr[j]
return arr
if __name__ == '__main__':
array = [25, 17, 33, 17, 22, 13, 32, 15, 9, 25, 27, 18]
print(bubbleSort(array))
选择排序(Selection Sort)基本思想:
第 i 趟排序从序列的后 n − i + 1 ( i = 1 , 2 , … , n − 1 ) n − i + 1 (i = 1, 2, …, n − 1) n−i+1(i=1,2,…,n−1)个元素中选择一个值最小的元素与该 n − i + 1 n - i + 1 n−i+1 个元素的最前面那个元素交换位置,即与整个序列的第 i 个位置上的元素交换位置。如此下去,直到 i = = n − 1 i == n − 1 i==n−1,排序结束。
可以简述为:每一趟排序中,从剩余未排序元素中选择一个最小的元素,与未排好序的元素最前面的那个元素交换位置。
def selectSort(arr):
for i in range(len(arr)-1):
min_i=i
for j in range(i+1,len(arr)):
if arr[j]<arr[min_i]:
min_i=j
if i!=min_i:
arr[i],arr[min_i]=arr[min_i],arr[i]
return arr
if __name__ == '__main__':
array = [25, 17, 33, 17, 22, 13, 32, 15, 9, 25, 27, 18]
print(selectSort(array))
将整个序列切分为两部分:前 i - 1
个元素是有序序列,后 n - i + 1
个元素是无序序列。每一次排序,将无序序列的首元素,在有序序列中找到相应的位置并插入。
可以简述为:每一趟排序中,将剩余无序序列的第一个元素,插入到有序序列的适当位置上。
n
个元素的序列,插入排序方法一共要进行 n - 1
趟排序。temp
。i
值只进行一次元素之间的比较,因而总的比较次数最少,为 ∑ i = 2 n 1 = n − 1 \sum_{i=2}^n1=n-1 ∑i=2n1=n−1,并不需要移动元素(记录),这是最好的情况。def insertSort(arr):
for i in range(1,len(arr)):
temp=arr[i]
j=i
while j>0 and arr[j-1]>temp:
arr[j]=arr[j-1]
j-=1
arr[j]=temp
return arr
if __name__=='__main__':
array = [25, 17, 33, 17, 22, 13, 32, 15, 9, 25, 27, 18]
print(insertSort(array))
本小节介绍四个排序算法,分别是希尔排序、归并排序、快速排序和堆排序。基于上面的三种算法进行适当改进,时间复杂度得到提升,但也会出现算法不稳定的一些情况。因此熟练掌握上面三种基本算法是比较排序的根本。
将整个序列切按照一定的间隔取值划分为若干个子序列,每个子序列分别进行插入排序。然后逐渐缩小间隔进行下一轮划分子序列和插入排序。直至最后一轮排序间隔为 1
,对整个序列进行插入排序。
gap
之间的依赖关系,并给出完整的数学分析。n
个元素的序列,若 g a p 1 = ⌊ n / 2 ⌋ gap_1=\lfloor n/2 \rfloor gap1=⌊n/2⌋ ,则经过 p = ⌊ l o g 2 n ⌋ p=\lfloor log_2 n \rfloor p=⌊log2n⌋ 趟排序后就有 ,因此,希尔排序方法的排序总趟数为 ⌊ l o g 2 n ⌋ \lfloor log_2 n \rfloor ⌊log2n⌋ 。while
循环为 l o g 2 n log_2 n log2n 数量级,中间层 do-while
循环为 n
数量级。当子序列分得越多时,子序列内的元素就越少,最内层的 for
循环的次数也就越少;反之,当所分的子序列个数减少时,子序列内的元素也随之增多,但整个序列也逐步接近有序,而循环次数却不会随之增加。因此,希尔排序算法的时间复杂度在 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)与 O ( n 2 ) O(n^2) O(n2)之间。def shellSort(arr):
size=len(arr)
gap=size//2
while gap>0:
for i in range(gap,size):
temp=arr[i]
j=i
while j>=gap and arr[j-gap]>temp:
arr[j]=arr[j-gap]
j-=gap
arr[j]=temp
gap=gap//2
return arr
if __name__=='__main__':
array = [25, 17, 33, 17, 22, 13, 32, 15, 9, 25, 27, 18]
print(insertSort(array))
采用分治策略,先递归地将当前序列平均分成两半。然后将有序序列两两合并,最终合并成一个有序序列。
merge(left_arr, right_arr)
的时间复杂度是 O ( n ) O(n) O(n),因此,归并排序算法总的时间复杂度为 O ( n l o g 2 n ) O(nlog_2 n) O(nlog2n)。merge(left_arr, right_arr)
算法能够使前一个序列中那个相同元素先被复制,从而确保这两个元素的相对次序不发生改变。所以归并排序算法是 稳定排序算法。#归并排序
#分
def merge(arr):
if len(arr)==1:
return arr
size=len(arr)
mid=size//2
left,right=arr[:mid],arr[mid:]
return mergesort(merge(left),merge(right))
#并
def mergesort(left,right):
arr=[]
while left and right:
if left[0]<=right[0]:
arr.append(left.pop(0))
else:
arr.append(right.pop(0))
arr+=left
arr+=right
return arr
arr=[6,5,3,1,8,7,2,4]
merge(arr)
通过一趟排序将无序序列分为独立的两个序列,第一个序列的值均比第二个序列的值小。然后递归地排列两个子序列,以达到整个序列有序。
在参加排序的元素初始时已经有序的情况下,快速排序方法花费的时间最长。此时,第 1
趟排序经过 n - 1
次比较以后,将第1
个元素仍然确定在原来的位置上,并得到 1
个长度为 n - 1
的子序列;第 2
趟排序进过n - 2
次比较以后,将第 2
个元素确定在它原来的位置上,又得到 1
个长度为 n - 2
的子序列;依次类推,最终总的比较次数为 ( n − 1 ) + ( n − 2 ) + . . . + 1 = n ( n − 1 ) 2 (n-1)+(n-2)+...+1=\frac{n(n-1)}{2} (n−1)+(n−2)+...+1=2n(n−1)。因此时间复杂度为 O ( n 2 ) O(n^2) O(n2) 。
还有一种情况,若每趟排序后,分界元素正好定位在序列的中间,从而把当前参加排序的序列分成大小相等的前后两个子序列,则对长度为 n 的序列进行快速排序所需要的时间为:
T ( n ) ≤ n + 2 T ( n / 2 ) ≤ 2 n + 4 T ( n / 2 ) ≤ 3 n + 8 T ( n / 8 ) . . . ≤ ( l o g 2 n ) n + n T ( 1 ) = O ( n l o g 2 n ) T(n)\le n+2T(n/2)\le 2n+4T(n/2)\le 3n+8T(n/8)...\le(log_2n)n+nT(1)=O(nlog_2n) T(n)≤n+2T(n/2)≤2n+4T(n/2)≤3n+8T(n/8)...≤(log2n)n+nT(1)=O(nlog2n)因此,快速排序方法的时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n) ,时间性能显然优于前面讨论的几种排序算法。
无论快速排序算法递归与否,排序过程中都需要用到堆栈或其他结构的辅助空间来存放当前待排序序列的首、尾位置。最坏的情况下,空间复杂度为 O ( n ) O(n) O(n) 。
若对算法进行一些改写,在一趟排序之后比较被划分所得到的两个子序列的长度,并且首先对长度较短的子序列进行快速排序,这时候需要的空间复杂度可以达到 O ( l o g 2 n ) O(log_2n) O(log2n) 。
快速排序时一种 不稳定排序算法,也是一种不适合在链表结构上实现的排序算法。
def quick_sort(lists,i,j):
if i >= j:
return list
pivot = lists[i]
low = i
high = j
while i < j:
while i < j and lists[j] >= pivot:
j -= 1
lists[i]=lists[j]
while i < j and lists[i] <=pivot:
i += 1
lists[j]=lists[i]
lists[j] = pivot
quick_sort(lists,low,i-1)
quick_sort(lists,i+1,high)
return lists
if __name__=="__main__":
lists=[30,24,5,58,18,36,12,42,39]
print("排序前的序列为:")
for i in lists:
print(i,end =" ")
print("\n排序后的序列为:")
for i in quick_sort(lists,0,len(lists)-1):
print(i,end=" ")
借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆维持大顶堆性质。
堆:符合以下两个条件之一的完全二叉树:
d
,算法由两个独立的循环组成:
1
个循环构造初始堆积时,从 i = d - 1
层开始,到 i = 1
层为止,对每个分支节点都要调用一次 adjust
算法,每一次 adjust
算法,对于第 i
层一个节点到第d
层上建立的子堆积,所有节点可能移动的最大距离为该子堆积根节点移动到最后一层(第 d
层) 的距离即 d - i
。i
层上节点最多有 2 i − 1 2^{i-1} 2i−1个,所以每一次 adjust
算法最大移动距离为 2 i − 1 ∗ ( d − i ) 2^{i-1}*(d-i) 2i−1∗(d−i)。因此,堆积排序算法的第 1
个循环所需时间应该是各层上的节点数与该层上节点可移动的最大距离之积的总和,即:2
个循环中每次调用 adjust
算法一次,节点移动的最大距离为这棵完全二叉树的深度n - 1
次adjust
算法,所以,第 2
个循环的时间花费为def heapify(arr, n, i):
largest = i
l = 2 * i + 1 # left = 2*i + 1
r = 2 * i + 2 # right = 2*i + 2
if l < n and arr[largest] < arr[l]:
largest = l
if r < n and arr[largest] < arr[r]:
largest = r
if largest != i:
arr[i],arr[largest] = arr[largest],arr[i] # 交换
heapify(arr, n, largest)
def heapSort(arr):
n = len(arr)
# Build a maxheap.
for i in range(n, -1, -1):
heapify(arr, n, i)
# 一个个交换元素
for i in range(n-1, 0, -1):
arr[i], arr[0] = arr[0], arr[i] # 交换
heapify(arr, i, 0)
arr = [ 12, 11, 13, 5, 6, 7]
heapSort(arr)
n = len(arr)
print ("排序后")
for i in range(n):
print ("%d" %arr[i])
前面介绍的排序算法都是基于比较的排序算法,时间复杂度最小为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n),本小节介绍的排序是非比较的思想,采用以时间换空间的策略,将介绍三种排序方法:计数排序,桶排序以及基数排序。
使用一个额外的数组 counts
,其中第i
个元素 counts[i]
是待排序数组arr
中值等于i
的元素个数。然后根据数组counts
来将arr
中的元素排到正确的位置。
n
个0 ~ k
之间的整数时,计数排序的时间复杂度为 O ( n + k ) O(n+k) O(n+k) 。counts
的长度取决于待排序数组中数据的范围(等于待排序数组最大值减去最小值再加 1
)。所以计数排序对于数据范围很大的数组,需要大量的时间和内存。def counting_sort(arr):
if len(arr) < 2:
return arr
max_num = max(arr)
count = [0] * (max_num + 1)
for num in arr:
count[num] += 1
new_array = []
for i in range(len(count)):
for j in range(count[i]):
new_array.append(i)
return new_array
if __name__ == '__main__':
array = [5, 7, 3, 7, 2, 3, 2, 5, 9, 5, 7, 6]
print(counting_sort(array))
将未排序的数组分到若干个「桶」中,每个桶的元素再进行单独排序。
n
,桶的个数是 m
时,桶排序时间复杂度为 O ( n + m ) O(n+m) O(n+m)。o(n + m)
。def bucket_sort(array):
min_num, max_num = min(array), max(array)
bucket_num = (max_num-min_num)//3 + 1
buckets = [[] for _ in range(int(bucket_num))]
for num in array:
buckets[int((num-min_num)//3)].append(num)
new_array = []
for i in buckets:
for j in sorted(i):
new_array.append(j)
return new_array
if __name__ == '__main__':
array = [5, 7, 3, 7, 2, 3, 2, 5, 9, 5, 7, 8]
print(bucket_sort(array))
将整数按位数切割成不同的数字,然后按每个位数分别比较进行排序。
n
是待排序元素的个数,k
是数字位数。k
的大小取决于数字位的选择(十进制位、二进制位)和待排序元素所属数据类型全集的大小。def radixSort(arr):
size = len(str(max(arr)))
for i in range(size):
buckets = [[] for _ in range(10)]
for num in arr:
buckets[num // (10**i) % 10].append(num)
arr.clear()
for bucket in buckets:
for num in bucket:
arr.append(num)
return arr
if __name__ == '__main__':
array = [25, 17, 33, 17, 22, 13, 32, 15, 9, 25, 27, 18]
print(radixSort(array))
以上是要介绍的所有排序算法的思路及方法,排序算法是数据结构基础中的基础,因此搞明白其含义以及熟练书写代码是基本操作了。后面会结合leetcode中的题目进行排序算法的进一步练习,此外还有一些出现频次不高的排序算法没有涉及。
敬请期待~
[1] 算法通关手册
[2] DataWhale第31期组队学习-leetcode刷题