冒泡排序:
冒泡排序(英语:Bubble Sort)是一种简单的排序算法。它重复地遍历要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。遍历数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序算法的运作如下:
-
比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。
-
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
-
针对所有的元素重复以上的步骤,除了最后一个。
-
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
[8, 9, 7, 6, 5, 4, 3, 2, 1]第1趟 遍历前: [8,9,7,6,5,4,3,2,1] => [8,7,6,5,4,3,2,1, 9] 遍历后
第2趟 遍历前: [8,7,6,5,4,3,2,1, 9] => [7,6,5,4,3,2,1, 8,9] 遍历后
第3趟 遍历前: [7,6,5,4,3,2,1, 8,9] => [6,5,4,3,2,1, 7,8,9] 遍历后
第4趟 遍历前: [6,5,4,3,2,1, 7,8,9] => [5,4,3,2,1, 6,7,8,9] 遍历后
第5趟 遍历前: [5,4,3,2,1, 6,7,8,9] => [4,3,2,1, 5,6,7,8,9] 遍历后
第6趟 遍历前: [4,3,2,1, 5,6,7,8,9] => [3,2,1, 4,5,6,7,8,9] 遍历后
第7趟 遍历前: [3,2,1, 4,5,6,7,8,9] => [2,1, 3,4,5,6,7,8,9] 遍历后
第8趟 遍历前: [2,1, 3,4,5,6,7,8,9] => [1, 2,3,4,5,6,7,8,9] 遍历后
l = [8, 9, 7, 6, 5, 4, 3, 2, 1]
for i in range(len(l)- 1): # i =0,1,2,3,4,5,6,7 共8趟
for j in range(len(l)-1-i): # j 遍历剩下未排好序的数
if l[j] > l[j+1]:
l[j], l[j+1] = l[j+1], l[j]
print(l)
时间复杂度
- 最优时间复杂度:O(n) (表示遍历一次发现没有任何可以交换的元素,排序结束。)
- 最坏时间复杂度:O(n2)
- 稳定性:稳定
选择排序
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
[8, 9, 7, 6, 5, 4, 3, 2, 1]
第1趟 遍历前: [8,9,7,6,5,4,3,2,1] => [1, 9,7,6,5,4,3,2,8] 遍历后
第2趟 遍历前: [1, 9,7,6,5,4,3,2,8] => [1,2, 7,6,5,4,3,9,8] 遍历后
第3趟 遍历前: [1,2, 7,6,5,4,3,9,8] => [1,2,3, 6,5,4,7,9,8] 遍历后
第4趟 遍历前: [1,2,3, 6,5,4,7,9,8] => [1,2,3,4, 5,6,7,9,8] 遍历后
第5趟 遍历前: [1,2,3,4, 5,6,7,9,8] => [1,2,3,4,5, 6,7,9,8] 遍历后
第6趟 遍历前: [1,2,3,4,5, 6,7,9,8] => [1,2,3,4,5,6, 7,9,8] 遍历后
第7趟 遍历前: [1,2,3,4,5,6, 7,9,8] => [1,2,3,4,5,6,7, 9,8] 遍历后
第8趟 遍历前: [1,2,3,4,5,6,7, 9,8] => [1,2,3,4,5,6,7,8, 9] 遍历后
l = [8, 9, 7, 6, 5, 4, 3, 2, 1]
# 方法一
for i in range(len(l)-1): # i =0,1,2,3,4,5,6,7 共8趟,每趟找最小数的下标
# 找到剩下未排序的数中的最小下标
min_index = i
for j in range(i, len(l)):
if l[j] < l[min_index]:
min_index = j
# 用剩下数中的最小数和第i个(剩下数中的第一个)元素交换
l[i], l[min_index] = l[min_index], l[i]
print(l)
# 方法二
for i in range(len(l)-1):
for j in range(i+1, len(l)-1):
if l[i]> l[j]:
l[i], l[j] = l[j], l[i]
print(l)
时间复杂度
- 最优时间复杂度:O(n2)
- 最坏时间复杂度:O(n2)
- 稳定性:不稳定(考虑升序每次选择最大的情况)
插入排序
插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
# 从第二个位置,即下标为1的元素开始向前插入
for i in range(1, len(l)):
# 从第i个元素开始向前比较,如果小于前一个元素,交换位置
for j in range(i, 0, -1):
if l[j] < l[j-1]:
l[j], l[j-1] = l[j-1], l[j]
print(l)
时间复杂度
- 最优时间复杂度:O(n) (升序排列,序列已经处于升序状态)
- 最坏时间复杂度:O(n2)
- 稳定性:稳定
快速排序
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤为:
- 从数列中挑出一个元素,称为"基准"(pivot),
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
[2, 4, 8, 5, 6, 7, 3, 0, 9, 1]
[ ] [ ] [ ]
方法一(简单易懂, 但是时间复杂度不是最优):
def quick_sort(l):
# 临界值
if len(l) < 2:
return l
# 先取一个中间值或者随机弹出一个
mid = l.pop(len(l)//2) # mid = l.pop()
left = [] # 存放大于m的数 left = [i for i in l: if i<=mid)
right = [] # 存放大于m的数 right = [i for i in l: if i>mid)
for n in l:
if n<mid:
left.append(n)
else:
right.append(n)
# print('left:', left)
# print('right:', right)
# 递归
return quick_sort(left) + [mid] +quick_sort(right)
ll = [2, 4, 8, 5, 6, 7, 3, 0, 9, 1]
print(quick_sort(ll))
方法二(时间复杂度最优):
def quick_sort(l, start=0, end=len(l)-1):
# 递归的退出条件
if start >= end:
return
# 定义两个指针,一个指针指向第一个元素,另一个指针指向最后一个元素
pre = start
next = end
# 设置基准元素
mid = l[start]
# 如果pre指针和next重合时跳出循环并把mid赋值给pre或者next
while pre < next:
# 如果next所指向的元素比基准元素大,就把next指针向前移动一位,否则跳出循环后把next所指向的元素赋值给pre
while l[next] >= mid and pre < next:
next -= 1
l[pre] = l[next]
# 如果pre所指向的元素比基准元素小,就把pre指针向后移动一位,否则跳出循环后把pre所指向的元素赋值给next
while l[pre] < mid and pre < next:
pre += 1
l[next] = l[pre]
l[pre] = mid
# 递归调用函数对基准值左边的数进行排序
quick_sort(l,start,pre-1)
# 递归调用函数度基准值右边的数进行排序
quick_sort(l,pre+1,end)
quick_sort(l)
print(l)
时间复杂度
- 最优时间复杂度:O(nlogn)
- 最坏时间复杂度:O(n2)
- 稳定性:不稳定
从一开始快速排序平均需要花费O(n log n)时间的描述并不明显。但是不难观察到的是分区运算,数组的元素都会在每次循环中走访过一次,使用O(n)的时间。在使用结合(concatenation)的版本中,这项运算也是O(n)。
在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为一的数列前,我们只要作log n次嵌套的调用。这个意思就是调用树的深度是O(log n)。但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;因此,程序调用的每一层次结构总共全部仅需要O(n)的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有O(n)个调用,这些被归纳在O(n)系数中)。结果是这个算法仅需使用O(n log n)时间。