本文通过阅读、观看大量文章和视频,筛选出较优质的文章并实际运行、验证代码而作,文章部分内容进行了参考(阅读的大量文章中不乏水文,就算文章不错的也有部分错误,使得读者难以理解或理解错误,这也是我作此文的目的),文末附有这些我觉得比较好的文章的链接分享,不迷路哦,如文章中有错误之处,请指正,谢谢n_n
个人建议:每个算法的代码至少敲十遍。孰能生巧,下笔有神。
排序, 就是重新排列表中的元素, 使表中的元素满足按关键字递增或递减的过程。为了査找方便,通常要求计算机中的表是按关键字有序的 。 排序的确切定义如下:
输 入: n个 记 录 R1,R2,R3…Rn, 对对应的关键字为K1,K2,K3…Kn
输出: 输入序列的一个重排R1’,R2’,R3’…Rn’, 使得有K1’ ≤ K2’ ≤ K3’… ≤ Kn’ (其中 ≤可以换成其它的比较大小符号)。
排序方法 | 时间复杂度(最坏) | 时间复杂度(平均) | 时间复杂度(最好) | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
插入排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 |
希尔排序 | O(n1.3) | O(n2) | O(n) | O(1) | 不稳定 |
选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(1) | 不稳定 |
冒泡排序 | O(n2) | O(n2) | O(n) | O(1) | 稳定 |
快速排序 | O(nlog2n) | O(n2) | O(nlog2n) | O(nlog2n) | 不稳定 |
归并排序 | O(nlog2n) | O(nlog2n) | O(nlog2n) | O(n) | 稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(n+k) | 稳定 |
基数排序 | O(n×k) | O(n×k) | O(n×k) | O(n+k) | 稳定 |
桶排序 | O(n+k) | O(n2) | O(n) | O(n+k) | 稳定 |
冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误(逆序)就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
算法描述:比较相邻的元素。如果第一个比第二个大,就交换它们两个;对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;针对所有的元素重复以上的步骤,除了最后一个;重复步骤1~3,直到排序完成。
演示动画:
python代码实现:
li = [12,23,1,34,89,4,76,2,43,9,45]
for i in range(len(li)):
for j in range(len(li)-1-i):
if li[j]>li[j+1]:
li[j],li[j+1] = li[j+1],li[j]
print(li)
代码就是这么简单,哈哈。
算法分析:若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值:Cmin = N - 1, Mmin = 0。所以,冒泡排序最好时间复杂度为O(N)。若初始文件是逆序的,需要进行 N -1 趟排序。每趟排序要进行 N - i 次关键字的比较(1 ≤ i ≤ N - 1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
Cmax = N(N-1)/2 = O(N2)
Mmax = 3N(N-1)/2 = O(N2)
冒泡排序的最坏时间复杂度为O(N2)。
因此,冒泡排序的平均时间复杂度为O(N2)。
快速排序算法按照字面意思就是时间复杂度"很快’的排序算法,在所有排序中,快速排序是最快的排序算法。一般的算法复杂度为O(n^2),但是快速排序法的时间复杂度为O(nlogn),所以说快速排序法在排序算法中最快,而且快速排序法不需要额外的内存。
快速排序原理:假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让第一个数6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列。
3 1 2 5 4 6 9 7 10 8
在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。想一想,你有办法可以做到这点吗?
方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=0,指向数字6)。让哨兵j指向序列的最右边(即j=9,指向数字8)。
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下。
6 1 2 5 9 3 4 7 10 8
到此,第一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下。
6 1 2 5 4 3 9 7 10 8
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。
交换之后的序列如下。
3 1 2 5 4 6 9 7 10 8
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。
OK,解释完毕。现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧。
左边的序列是“3 1 2 5 4”。请将这个序列以3为基准数进行调整,使得3左边的数都小于等于3,3右边的数都大于等于3。好了开始动笔吧。如果你模拟的没有错,调整完毕之后的序列的顺序应该是。
2 1 3 5 4
OK,现在3已经归位。接下来需要处理3左边的序列“2 1”和右边的序列“5 4”。对序列“2 1”以2为基准数进行调整,处理完毕之后的序列为“1 2”,到此2已经归位。序列“1”只有一个数,也不需要进行任何处理。至此我们对序列“2 1”已全部处理完毕,得到序列是“1 2”。序列“5 4”的处理也仿照此方法,最后得到的序列如下。
1 2 3 4 5 6 9 7 10 8
对于序列“9 7 10 8”也模拟刚才的过程,直到不可拆分出新的子序列为止。最终将会得到这样的序列,如下。
1 2 3 4 5 6 7 8 9 10
到此,排序完全结束。细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。下面上个霸气的GIF图来描述下整个算法的处理过程:
python代码实现:
def quickly_sorted(start,end,data):
if start>=end:
return data
else :
base=data[start]
i=start
j=end-1
while i<j:
if data[j]<base :##首先右指针移动
if data[i]>base:###左指针移动
data[i],data[j]=data[j],data[i]##满足条件交换
i+=1
j-=1
else :
i+=1###若左指针不满足 移动左指针
else :
j-=1###右指针不满足 移动右指针
if base>data[i]:
data[start],data[i]=data[i],data[start]
quickly_sorted(start,i,data)###重复上面的步骤
quickly_sorted(i+1,end,data)
return data
data=[6,1,2,7,9,3,4,5,10,8]
start=0
end=len(data)
quickly_sorted(start,end,data)
快速排序之所比较快,因为相比冒泡排序,每次交换是跳跃式的。每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(N2),它的平均时间复杂度为O(NlogN)。
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
算法描述:
lst = [6,1,2,7,9,3,4,5,10,8]
def insertSort(arr):
for i in range(1,len(arr)):
j = i-1
key = arr[i]
while j >= 0:
if arr[j] > key:
arr[j+1] = arr[j]
arr[j] = key
j -= 1
return arr
print(insertSort(lst))
算法分析:插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
算法描述:
这里我们通过例子进行讲解。
例:对下面这九个人按照身高从小到大进行排序。
第1步:分组
把待排序列,分成多个间隔为h的子序列,这里我们选取4,h通常为总数的一半,奇偶均可,(在希尔排序中其实对分组间隔没有明确要求,可以随意选取,你也可以选3),这里我们将人分为4组,我们用颜色进行标记。
第1轮组内排序,分组间隔为4
第2步:组内排序
组内进行比较,对每个子序列进行插入排序(没错,就是插入排序,原因后面再作介绍,不要急),互换结果为:
重新设置间隔分组,分组间隔为之前的一半,为2。重新分组后颜色标记如下:
第2轮组内排序,还是利用插入排序对每个组的组内进行排序。排序结果为:
重新进行分组,分组间隔为之前的一半,为1。
同样,进行组内排序,结果为:
到此,排序已经完成。
我们对希尔排序进行总结:
现在回答之前的疑问:为什么在组内排序过程中使用到了插入排序,那么为什么不直接使用插入排序呢,这样希尔排序比插入排序有些步骤不就多余了吗?
如果我们对一个10000长度的无序数列进行排序,我们进行了统计,数据如下:
很明显,希尔排序的比较和交换次数要低很多很多,而且序列的长度越长,希尔排序效果越明显。如果直接使用插入排序的话效率会很低。
希尔排序是基于插入排序的以下两点性质而提出改进方法的:
时间复杂度:最坏情况下为O(n^2),平均时间复杂度为O(nlogn);
空间复杂度:归并排序需要一个大小为1的临时存储空间用以保存合并序列,所以空间复杂度为O(1)
算法稳定性:不稳定
懂了之后,这里我们附上GIF图
好了,上代码!
def shell_sort(li):
n = len(li)
gap = n // 2 # 设置初始间隔,值为序列长度的一半,并进行分组
while gap > 0:
for i in range(gap, n):
# 使用插入排序进行组内排序
while i >= gap and li[i - gap] > li[i]:
li[i - gap], li[i] = li[i], li[i - gap]
i -= gap
gap = gap // 2 # 重新设置分组间隔,为之前的一半
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
shell_sort(li)
print(li)
废话不多说,选择排序就是:在待排序列中,从左到右进行扫描,记录最大的数,扫描过一遍之后,将序列最后一位与序列中最大值进行交换(如果最大值就是最后一位,则不变换位置,你也可以理解成自己和自己交换),这样,序列中最大数到达最后面。然后同样的做法,对序列从0到倒数第2位进行扫描,找到最大值并与倒数第2位交换,重复操作,直到待排序列区只有一位。
其实,有些方法是相反的,这种方法是将待排序列中找最小数与第1位数进行互换,但是道理都是一样的。
下面的GIF图中演示的就是我说的第2中方法,找最小的数:
python实现代码:
def selection_sort(li):
for i in range(len(li)-1):
min_index = i #假设将每次开始的第一个元素的索引作为最小值的索引
for j in range(i,len(li)): #从i开始循环,说明每次循环的次数依次减少
if li[min_index] > li[j]:
min_index = j #将最小值的索引赋值给min_index
li[min_index], li[i] = li[i], li[min_index] # 每次将每行剩余的最小的值赋值给开始循环的第一个值
if __name__ in '__main__':
li = [3,6,5,8,9,0]
selection_sort(li)
print(li)
算法分析:表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
什么是堆:对构建的完全二叉树,如果所有父节点的值大于子节点的值,则成为堆(也叫大顶堆,反之为小顶堆),构建方法:
建好大顶堆(2)后进行堆排序:
算法完整展示GIF图:
python实现代码:
def HeapSort(input_list):
#调整parent结点为大根堆
def HeapAdjust(input_list,parent,length):
temp = input_list[parent]
child = 2*parent+1
while child < length:
if child+1 <length and input_list[child] < input_list[child+1]:
child +=1
if temp > input_list[child]:
break
input_list[parent] = input_list[child]
parent = child
child = 2*child+1
input_list[parent] = temp
if input_list == []:
return []
sorted_list = input_list
length = len(sorted_list)
#最后一个结点的下标为length//2-1
#建立初始大根堆
for i in range(0,length // 2 )[::-1]:
HeapAdjust(sorted_list,i,length)
for j in range(1,length)[::-1]:
#把堆顶元素即第一大的元素与最后一个元素互换位置
temp = sorted_list[j]
sorted_list[j] = sorted_list[0]
sorted_list[0] = temp
#换完位置之后将剩余的元素重新调整成大根堆
HeapAdjust(sorted_list,0,j)
print('%dth' % (length - j))
print(sorted_list)
return sorted_list
if __name__ == '__main__':
input_list = [50,123,543,187,49,30,0,2,11,100]
print("input_list:")
print(input_list)
sorted_list = HeapSort(input_list)
print("sorted_list:")
print(input_list)
算法分析:堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法描述:
1.将序列中带排序数字分为若干组,每个数字分为一组(序列的长度为n,则分为n组)
2.将若干个组两两合并,保证合并后的组是有序的。(将两个组的第1个数进行比较,取出较大的,那么这一组中第2个数就成了第1个数)
3.重复第二步操作直到只剩下一-组,排序完成
B站视频推荐-归并排序算法讲解
GIF图展示:
python实现代码:
def merge(a, b):
c = []
h = j = 0
while j < len(a) and h < len(b):
if a[j] < b[h]:
c.append(a[j])
j += 1
else:
c.append(b[h])
h += 1
if j == len(a):
for i in b[h:]:
c.append(i)
else:
for i in a[j:]:
c.append(i)
return c
def merge_sort(lists):
if len(lists) <= 1:
return lists
middle = len(lists)//2
left = merge_sort(lists[:middle])
right = merge_sort(lists[middle:])
return merge(left, right)
if __name__ == '__main__':
a = [14, 2, 34, 43, 21, 19]
print (merge_sort(a))
算法分析
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。
基本思想:分配+收集
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
算法描述:
假设对:[614,738,921,485,637,101,530,790,306]
进行排序。
1.首先将每个数按照个位数的大小分配到不同的桶里。
2.将桶里的数从0-9收集回来
3.同样的道理,将每个数按照十位的大小分配到不同的桶里。
4.按照百位分配
B站视频推荐-基数排序
GIF图展示:
python实现代码:
def RadixSort(a):
i = 0 #初始为个位排序
n = 1 #最小的位数置为1(包含0)
max_num = max(a) #得到带排序数组中最大数
while max_num > 10**n: #得到最大数是几位数
n += 1
while i < n:
bucket = {} #用字典构建桶
for x in range(10):
bucket.setdefault(x, []) #将每个桶置空
for x in a: #对每一位进行排序
radix =int((x / (10**i)) % 10) #得到每位的基数
bucket[radix].append(x) #将对应的数组元素加入到相应位基数的桶中
j = 0
for k in range(10):
if len(bucket[k]) != 0: #若桶不为空
for y in bucket[k]: #将该桶中每个元素
a[j] = y #放回到数组中
j += 1
i += 1
if __name__ == '__main__':
a = [12,3,45,3543,214,1,4553]
print("Before sorting...")
print(a)
print("---------------------------------------------------------------")
RadixSort(a)
print("After sorting...")
print(a)
算法分析:
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法描述:
1.找出待排序的数组中最大和最小的元素(以便准备待排序列的桶,比如待排序列最小值为3,最大值为99,则我们准备编号为3-99的桶);
2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项(将序列中的数与桶的编号进行对应,放入桶中,记录每个桶中的个数,也就是该桶编号的数的出现次数);
3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1,完成排序。
GIF图展示:
python实现代码:
from numpy.random import randint
def ConutingSort(A):
k = max(A) # A的最大值,用于确定C的长度
C = [0]*(k+1) # 通过下表索引,临时存放A的数据
B = (len(A))*[0] # 存放A排序完成后的数组
for i in range(0, len(A)):
C[A[i]] += 1 # 记录A有哪些数字,值为A[i]的共有几个
for i in range(1, k+1):
C[i] += C[i-1] # A中小于i的数字个数为C[i]
for i in range(len(A)-1, -1, -1):
B[C[A[i]]-1] = A[i] # C[A[i]]的值即为A[i]的值在A中的次序
C[A[i]] -= 1 # 每插入一个A[i],则C[A[i]]减一
return B
A = list(randint(1, 99, 10))
print(A)
A = ConutingSort(A)
print(A)
算法分析:
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
算法描述:
GIF图展示:
python实现代码:
def bucketSort(nums):
max_num = max(nums) # 选择一个最大的数
bucket = [0]*(max_num+1) # 创建一个元素全是0的列表, 当做桶
for i in nums: # 把所有元素放入桶中, 即把对应元素个数加一
bucket[i] += 1
sort_nums = [] # 存储排序好的元素
for j in range(len(bucket)): # 取出桶中的元素
if bucket[j] != 0:
for y in range(bucket[j]):
sort_nums.append(j)
return sort_nums
nums = [5,6,3,2,1,65,2,0,8,0]
print(bucketSort(nums))
算法分析:桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
参考文章(下面部分文章中部分内容有错误,但值得借鉴,请酌情参考):
1.十大排序算法图解
2.快速排序法原理及python实现代码
3.python快速排序 过程图解 ,形象描述快速排序过程
4.排序算法之python堆排序
如文章中有错误之处或者有哪些地方还是不懂,请留言,谢谢n_n