冒泡排序、选择排序、插入排序、快速排序、归并排序、计数排序的思想与实现

1.冒泡排序

# 冒泡排序在遍历时,每次只比较相邻的两个元素,并把较大的元素放在后面。

# 冒泡排序的每次遍历,得到的较大数的位置都会被确定。

是稳定的原地排序算法。

最好情况下,要排序的数据已经是有序的,只需要进行一次冒泡操作,最好情况时间复杂度是O(n);最坏的情况是要排序的数据刚好是倒序排列,需要进行n次冒泡操作,时间复杂度是O(n2)。

 1 def bubble_sort(alist):
 2     """冒泡排序"""
 3     n = len(alist)
 4     for j in range(0, n-1):
 5         sorted_flag = False  # 提前退出冒泡循环的标志位,减少不必要的比较,优化算法
 6         for i in range(0, n-1-j):
 7             if alist[i] > alist[i+1]:
 8                 alist[i], alist[i+1] = alist[i+1], alist[i]
 9                 sorted_flag = True
10         if sorted_flag is False:
11             break
12 
13 if __name__ == "__main__":
14     li = [12, 4, 32, 45, 333, 234, 23]
15     bubble_sort(li)
16     print(li)         # [4, 12, 23, 32, 45, 234, 333]


 

2.选择排序

选择排序算法的实现思路:数据划分已排序区间和未排序区间,选择排序每次会从未排序区间中找到最小的元素,将其与未排序区间的头部交换。

# 具体排序步骤:

A.列表分成:有序序列部分和无序序列部分,每次遍历从无序序列中选出最小的,放在有序的序列中;

B.第一次从位置0开始遍历,选出最小的元素与位置0的元素交换位置;

C.第二次从位置1开始遍历,选出次小的元素与位置1的元素交换位置;

D.第三次从位置2开始遍历,选出再次小的元素与位置2的元素交换位置;

E. ...。

# 是不稳定的原地排序算法。

# 最好、最坏时间复杂度都是O(n2)。

 1 def select_sort(alist):
 2     """选择排序"""
 3     n = len(alist)
 4     for j in range(0, n-1):  # j是遍历次数,无序序列最后剩下的元素肯定是最大值,不用遍历
 5         min_index = j  # 记录最小值位置
 6         for i in range(j+1, n):  # 从无序序列中选最小值
 7             if alist[min_index] > alist[i]:
 8                 min_index = i  # 找最小值所在位置
 9         alist[min_index], alist[j] = alist[j], alist[min_index]
10 
11 if __name__ == "__main__":
12     li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
13     select_sort(li)
14     print(li)        # [17, 20, 26, 31, 44, 54, 55, 77, 93]


 

3.插入排序

# 插入排序算法的思想:数据划分已排序区间和未排序区间,初始的已排序区间只有一个元素,即数组的第一个元素。然后取未排序区间的元素,在已排序区间中找到合适的插入位置将其插入,并保证已排序区间数据一直有序,重复这个过程,直到未排序区间中元素为空,算法结束。

# 具体排序步骤:

A.列表分成:有序序列部分和无序序列部分,每次选无序序列的第一个,插入到有序序列中的正确位置;

B.每次选出的无序序列的第一个元素,都从有序序列的最后一个元素往前与之比较;

C.若被选出的无序序列的第一个元素比有序序列的某个元素小,则互换位置(升序排列),反之找到正确的插入位置。

# 是稳定的原地排序算法。

# 最好情况下,要排序的数据已经是有序的,不需要搬移任何数据,只需要从尾到头遍历已经有序的数据,所以最好时间复杂度为

O(n);最坏情况下,数据是倒序的,每次插入都相当于在数据的第一个位置插入新的数据,需要移动大量的数据,所以最坏情况时间复杂度为 O(n2)。

 1 def insert_sort(alist):
 2     """插入排序"""
 3     n = len(alist)
 4     for j in range(1, n):  # 刚开始时,默认列表位置0的元素为最小值
 5         for i in range(j, 0, -1):  # 下标在有序序列中前移一位,然后继续比较
 6             # j也是有序序列中进行比较的起始位置,因为比较是从后往前进行
 7             if alist[i] < alist[i-1]:
 8                 alist[i], alist[i-1] = alist[i-1], alist[i]
 9             else:
10                 break  # 优化算法,减少不必要的比较
11 
12 if __name__ == "__main__":
13     li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
14     insert_sort(li)
15     print(li)


 

4.快速排序

# 快速排序算法利用的是分治思想:如果要排序数组的下标从p到r,选择p到r之间的任意一个数据作为pivot(分区点),遍历p到r之间的数据,将小于pivot的放到左边,将大于pivot的放到右边,将pivot放到中间。经过这一步骤之后,数组p到r之间的数据就被分成了三个部分,前面p到q-1之间都是小于pivot的,中间是pivot,后面的q+1到r之间是大于pivot的。

# 具体排序步骤:

A.在数据列表中找到一个分区点元素,小于分区点的数据都放在其左边,大于分区点的数据都放在其右边;

B.分区后的数据子列表的最左侧元素,可以保存为该分区子列表下一轮分区操作的分区点;

C.需要一个low游标指向小于分区点的数据,也需要一个high游标指向大于分区点的数据。在每个分区内,low从列表最左侧往右遍历,high从列表最右侧往左遍历;

D.最开始时,low指向第一个分区点,分区点的值被保存下来,然后置空low指向的值;

E.如果选列表最左侧元素作为分区点,则应先从high游标开始比较;

F.如果high所指的值大于分区点,high左移一位,如果low所指的值小于分区点,low右移一位;

G.如果high指向的值小于分区点时,把该值移动至low所在的位置,置空high所指的值,并固定high的位置;此时low所指的值小于分区点,low右移;如果low指向的值大于分区点时,把该值移动至high所在的空位置,置空low所指的值,并固定low的位置;此时high所指的值大于分区点,high左移;

H.当low与high会合时,该位置就是分区点应该所处的正确位置。

# 是不稳定的原地排序算法。

# 最好情况下,如果每次分区操作,选择的分区点都很合适,正好能将大区间对等地一分为二,此时快速排序算法对应的的最好情况时间复杂度是O(nlogn);最坏情况下,如果数组中的数据原来已经是有序的,若每次选择最后一个元素作为分区点,则需要进行大约n次分区操作,才能完成快排的整个过程,每次分区平均要扫描大约n/2个元素,这种情况对应的最坏情况时间复杂度是O(n2)。

 1 def quick_sort(alist, first, last): # first是第一个元素的下标,last是最后一个元素的下标
 2     """快速排序"""
 3     if first >= last:  # 递归终止条件:当下标first不小于下标last时,排序结束
 4         return
 5     mid_value = alist[first]  # 保存分区点
 6     low, high = first, last
 7 
 8     while low < high:  # 控制high与low交替移动
 9         while low < high:  # 当游标还没有会合的时候,先比较high指向的数据
10             if alist[high] >= mid_value:  # '='只需在一边判断即可,以保证相等的数据都在分区点同侧
11                 high -= 1  # high左移
12             else:  break
13         alist[low] = alist[high]
14         while low < high:  # 当游标还没有会合的时候,再比较low指向的数据
15             if alist[low] < mid_value:
16                 low += 1  # low右移
17             else:  break
18         alist[high] = alist[low]
19     alist[low] = mid_value  # 大循环退出时,low=high,需要把保存的分区点放在该位置处
20 
21     # 由于快排只在一个数据列表内操作(原地排序),所以递归时不能使用列表切片作为子列表来传递参数
22     quick_sort(alist, first, low-1)  # 对分区点左边的子列表排序
23     quick_sort(alist, low+1, last)  # 对分区点右边的子列表排序
24 
25 if __name__ == "__main__":
26     li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
27     quick_sort(li, 0, len(li)-1)
28     print(li)   # [17, 20, 26, 31, 44, 54, 55, 77, 93]


 

5.归并排序

# 归并排序的核心思想:如果要排序一个数组,先把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就全部有序。

# 具体排序步骤:

A.归并排序需要先进行不断地拆分,直至拆分后的数据子序列只包含一个元素;

B.先两两比较并排序,然后两两合并形成新的子序列,接着再两两比较并排序合并后的子序列,然后再合并...;

C.在比较、合并时,需要一个left游标指向一个子序列的最左边,一个right游标指向另一个子序列的最左边;

D.left与right指向的数据进行比较,将较小的值存放进新列表,该值的游标右移一位,然后继续比较。

# 是稳定的非原地排序算法。

# 归并排序的执行效率与要排序的原始数组的有序程度无关,其时间复杂度非常稳定,最好情况时间复杂度、最坏情况时间复杂度都是O(nlogn)。

 1 def merge_sort(alist):
 2     """归并排序"""
 3     n = len(alist)
 4     if n <= 1:  # 递归拆分的终止条件
 5         return alist
 6     mid = n // 2
 7     left_li = merge_sort(alist[:mid])  # left与right是归并排序后,形成的新的有序子列表
 8     right_li = merge_sort(alist[mid:])
 9 
10     left_p, right_p = 0, 0
11     result = []  # 存放合并的结果值
12     while left_p < len(left_li) and right_p < len(right_li):  # 控制比较的过程
13         if left_li[left_p] <= right_li[right_p]:  # 存放较小值,等号保证相等值在同侧
14             result.append(left_li[left_p])
15             left_p += 1
16         else:
17             result.append(right_li[right_p])   # 存放较小值
18             right_p += 1
19     result += left_li[left_p:]  # 将left_li或right_li剩下的元素添加进来
20     result += right_li[right_p:]
21 
22     return result
23 
24 if __name__ == "__main__":
25     li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
26     sorted_li = merge_sort(li)  # 由于算法是非原地算法,排序后的结果存储在了另一个列表中
27     print(sorted_li)     # [17, 20, 26, 31, 44, 54, 55, 77, 93]


 

6.计数排序

# 计数排序要求要排序的n个数据所处的范围并不大,比如最大值是k,我们就可以把数据划分成k个桶。

# 每个桶内的数据值都是相同的,省掉了桶内排序的时间(如给高考考生排序)。

# 计数排序跟桶排序非常类似,只是桶的大小粒度不一样。

# 计数排序只能给非负整数排序,如果要排序的数据是其他类型的,要将其在不改变相对大小的情况下,转化为非负整数。

# 是稳定的原地排序。

# 因为计数排序只涉及扫描遍历操作,所以时间复杂度是O(n)。

 1 def countingSort(arr, max_value):
 2     """计数排序"""
 3     bucket_num = max_value+1  # 桶的数量
 4     bucket = bucket_num * [0]  # 生成空桶
 5 
 6     for i in range(len(arr)):  # 数据入桶
 7         bucket[arr[i]] += 1
 8 
 9     sorted_count = 0  # 对所有桶内的数据计数
10     for j in range(bucket_num):  # 数据出桶
11         while bucket[j] > 0:  # 每个桶内的数据
12             arr[sorted_count] = j
13             sorted_count += 1
14             bucket[j] -= 1
15     return arr
16 
17 if __name__ == '__main__':
18     array = [0, 2, 5, 5, 5, 6, 3, 1, 10, 9, 9, 7, 7, 7, 8]
19     sorted_arr = countingSort(array, 10)
20     print(sorted_arr)  # [0, 1, 2, 3, 5, 5, 5, 6, 7, 7, 7, 8, 9, 9, 10]

计数排序图片来源:https://www.runoob.com/w3cnote/counting-sort.html

posted @ 2020-05-25 15:47  孔子?孟子?小柱子!  阅读(381)  评论(0编辑  收藏  举报