大话数据结构 - 排序算法总结
本章讲到的排序算法,从算法的复杂性上分为两大类:简单算法(冒泡排序,简单选择排序,直接插入排序),改进算法(希尔排序,堆排序,归并排序,快速排序)。从整体上来讲,简单算法的时间复杂度大多为o(n2),改进算法的时间复杂度为o(nlogn),原因在于改进算法大都利用了二叉树的优点。下面对每种排序算法从以下几个方面进行详细的解释:1) 原始算法,2) 优化算法(如果存在),3) 时间复杂度,4) 空间复杂度,5) 优点,6) 缺点, 7) 改进点,8) 适用场景。
> 冒泡算法(Bubble Sort)
1. 原始算法:
冒泡算法的出现是为了解决最简单的交换排序中,得到每个位置排序结果的过程中,不会对其他位置的排序产生帮助。从而引申出一种类似冒泡过程的排序算法,从序列的最末端开始比较,遇到比它重的记录,它就会漂上来,遇到比它轻的记录,它就停在那里,让比它轻的记录继续上浮,但它也在这个过程中向上漂了一段距离。代码的实现如下:
1 def swap(x, y): 2 temp = L[x] 3 L[x] = L[y] 4 L[y] = temp 5 6 def BubbleSort(L): 7 for i in range(len(L)): 8 for j in range(len(L) - 2, i - 1, -1): 9 if L[j] > L[j + 1]: 10 swap(j, j + 1)
2. 优化算法:
由于原始算法在j的循环过程中,会两两比较位于序列后端的记录大小情况,如果已经满足排序的结果,则不会发生记录交换。这样就可以通过设置一个flag,判断是否应该进入内层循环,默认值为True,在内层循环的开始设为False,在发生交换时设为True,用此方式来判断是否已经完成了排序。代码如下:
1 def BubbleSort2(L): 2 flag = True 3 for i in range(len(L)): 4 if flag is True: 5 flag = False 6 for j in range(len(L) - 2, i - 1, -1): 7 if L[j] > L[j + 1]: 8 swap(j, j + 1) 9 flag = True
3. 时间复杂度:
对于优化算法来讲,最好情况是第一次比较就完成了排序,比较的次数为n-1,时间复杂度为o(n);最差情况是原序列是逆序,此时需要比较的次数为1+2+...+(n-1)=n(n-1)/2,时间复杂度为o(n2)。
4. 空间复杂度:
冒泡算法由于是两两交换,只需要一个额外的temp空间进行保存即可,因此空间复杂度可以认为是o(1)。
5. 优点:
6. 缺点:
7. 改进点:
8. 适用场景:
> 简单选择排序(Simple Selection Sort)
1. 原始算法:
简单选择,也就是按照序列的次序,依此找出当前剩余最小的记录,并和当前次序的记录进行交换。由于每次交换都已知是当前最合适的,因此简单选择排序的记录交换次数最少。代码实现如下:
1 def SelectSort(L): 2 for i in range(len(L)): 3 minus = i 4 for j in range(i, len(L)): 5 if L[minus] > L[j]: 6 minus = j 7 swap(i, minus)
2. 时间复杂度:
简单选择排序由于每个循环过程都需要比较,因此比较的次数为1+2+...+(n-1)=n(n-1)/2。对于交换次数来说,最好的情况是原序列就是正序,交换0次,最差情况是原序列是逆序,交换n-1次,时间复杂度依然为o(n2)。
3. 空间复杂度:
这里只需要一个额外的temp空间,空间复杂度为o(1)。
4. 优点:交换次数最少
5. 缺点:比较次数最多
6. 改进点:
7. 适用场景:
> 直接插入排序(Straight Insertion Sort)
1. 原始算法:
直接插入排序的思路是类似扑克牌的排序。假设我们有一手扑克牌是53462,首先将3插入到5的前面,得到35462;下面看4,4小于5而大于3,因此应该插入到35的中间,得到34562;6是正确排序,无需移动;2要比所有左侧的记录都小,因此其他记录依此向后移动,2插入到序列的最前端。总结起来就是,按顺序(这里是从左到右)搜索序列,每当遇到不符合排序规则的记录时,和前面的各记录比较,直到找到合适的位置插入。实现算法如下:
1 def InsertSort(L): 2 for i in range(1, len(L)): 3 print(L) 4 if L[i-1] > L[i]: 5 flag = L[i] 6 for j in range(i-1, -1, -1): 7 if L[j] > flag: 8 L[j+1] = L[j] 9 else: 10 break 11 if j == 0: 12 j -= 1 13 L[j+1] = flag
2. 时间复杂度:
最好的情况,序列本来就是有序的,那么比较的次数为o(n);最坏的情况下,序列是逆序的,此时需要比较2+3+...+n = (n+2)(n-1)/2,移动次数为3+4+..+(n+2) = (n+4)(n-1)/2。这样平均的比较和移动次数为n2/4,得到时间复杂度为o(n2),和冒泡排序和简单选择排序同样的时间复杂度,直接插入排序的效率也更高一些。
3. 空间复杂度:
同样是o(1)。
4. 优点:
5. 缺点:
6. 改进点:
7. 适用场景:
> 希尔排序(Shell Sort)
1. 原始算法:
希尔排序是针对直接插入排序的改进算法。基本思路是将序列进行分割,将相隔某个增量的记录组成一个子序列,进行直接插入排序;然后逐步减少增量,从粗到细的完成排序过程。代码实现如下:
1 def ShellSort(L): 2 increment = len(L) 3 while increment > 0: 4 increment = int(increment / 3) 5 for i in range(increment, len(L)): 6 if L[i] < L[i-increment]: 7 save = L[i] 8 j = i - increment 9 try: 10 while L[j] > save: 11 L[j + increment] = L[j] 12 j -= increment 13 except: 14 pass 15 L[j + increment] = save
2. 时间复杂度:
3. 空间复杂度:
4. 优点:
5. 缺点:
6. 改进点:
7. 适用场景:
> 堆排序(Heap Sort)
1. 原始算法:
堆排序的实现分成两个阶段。第一阶段是将序列重新排列成堆的方式。所谓堆,就是将序列看成完全二叉树,树上每个结点的值都大于它的子孙的值。通过根节点和左右孩子的比较,将序列里较大的值不断交换上移到二叉树的上层,在对每个结点都做类似的处理后,得到的序列就是大顶堆;如果把较小的值放在二叉树的上层,那么就是小顶堆。第二阶段就是将大顶堆的根节点和最后一个枝叶交换,那么最大值就放置在序列的最末尾,再将新的新结点对剩余的序列进行堆的处理,把第二大的值放置在二叉树的根节点,并和序列的倒数第二个结点交换,以此类推最终得到排序的结果。代码实现如下:
1 def swap(x, y): 2 temp = L[x] 3 L[x] = L[y] 4 L[y] = temp 5 6 def HeapArray(index, max_index): 7 if max_index == 0: 8 return 9 while True: 10 if index * 2 + 1 == max_index: 11 child = index * 2 + 1 12 else: 13 if L[index * 2 + 1] < L[index * 2 + 2]: 14 child = index * 2 + 2 15 else: 16 child = index * 2 + 1 17 if L[child] > L[index]: 18 swap(index, child) 19 index = child 20 if index * 2 + 1 > max_index: 21 break 22 23 def HeapSort(L): 24 for i in range(int(len(L)/2 - 1), -1, -1): 25 HeapArray(i, len(L) - 1) 26 for i in range(len(L) - 1, 0, -1): 27 swap(0, i) 28 HeapArray(0, i - 1)
2. 时间复杂度:
3. 空间复杂度:
4. 优点:
5. 缺点:
6. 改进点:
7. 适用场景:
> 归并排序(Merging Sort)
1. 原始代码(递归):
归并排序的思路是,通过将原序列按照中间的位置进行两两拆分,分别对这两部分进行分别排序后,归并到最终的结果中。通过递归不断拆分到最小记录,将比较后的结果参与上一层的排序,直到得到最终的排序结果。在归并的过程中,由于每个子序列已经是正确排序的,比较两个子序列的第一个元素,将最小的取出来并把index后移,参与和另一个序列当前记录的比较,直到两个子序列的所有记录都参与比较,这时就得到了正确的排序结果。代码实现如下:
1 def MergeSort(L): 2 MSort(L, 0, len(L) - 1) 3 4 def MSort(L, b, e): 5 if b == e: 6 return 7 else: 8 m = int((b + e) / 2) 9 MSort(L, b, m) 10 MSort(L, m + 1, e) 11 Merge(L, b, m, e) 12 13 def Merge(L, b, m, e): 14 index1 = b 15 index2 = m + 1 16 temp = [] 17 while True: 18 if index1 == m + 1 and index2 == e + 1: 19 break 20 elif index1 == m + 1: 21 temp.append(L[index2]) 22 index2 += 1 23 elif index2 == e + 1: 24 temp.append(L[index1]) 25 index1 += 1 26 elif L[index1] < L[index2]: 27 temp.append(L[index1]) 28 index1 += 1 29 elif L[index2] < L[index1]: 30 temp.append(L[index2]) 31 index2 += 1 32 for i in range(b, e + 1): 33 L[i] = temp[i - b]
2. 优化代码(非递归):
归并排序的非递归算法会减少因为递归带来的时间和空间损耗,并且思路更加简明。先将序列中两两记录进行排序,这样再把得到的结果按照4个一组进行排序,直到序列的长度。如果有散落的结尾序列存在,还要在排序结束前将之和前面的序列再做一次排序,代码如下:
1 def MergingSort2(L): 2 step = 2 3 while True: 4 start = 0 5 while True: 6 if start > len(L) - 1: 7 break 8 elif start + step <= len(L): 9 print(start, start + int((step - 1) / 2), start + step - 1) 10 Merge(L, start, start + int((step - 1) / 2), start + step - 1) 11 start = start + step 12 if step * 2 > len(L): 13 break 14 else: 15 step *= 2 16 if step is not len(L): 17 Merge(L, 0, step - 1, len(L) - 1)
3. 时间复杂度:
4. 空间复杂度:
5. 优点:
6. 缺点:
7. 改进点:
8. 适用场景:
> 快速排序(Quick Sort)
1. 原始代码:
快速排序的代码思路就是不断的找到序列首部记录的正确位置,并以它来分割其他部分为两个子序列,再对子序列进行同样的排序,代码如下:
1 def QuickSort(L): 2 QSort(L, 0, len(L) - 1) 3 4 def QSort(L, low, high): 5 if low < high: 6 pivot = Partition(L, low, high) 7 QSort(L, low, pivot - 1) 8 QSort(L, pivot + 1, high) 9 10 def Partition(L, low, high): 11 pivotkey = L[low] 12 while low < high: 13 while low < high and L[high] >= pivotkey: 14 high -= 1 15 swap(low, high) 16 while low < high and L[low] <= pivotkey: 17 low += 1 18 swap(low, high) 19 return low
2. 时间复杂度:
3. 空间复杂度:
4. 优点:
5. 缺点:
6. 改进点:
7. 适用场景: