排序算法可以分为内部排序和外部排序
- 待排序的记录数不太多:所有的记录都能存放在内存中进行排序,称为内部排序;
- 待排序的记录数太多:所有的记录不可能存放在内存中,排序过程中必须在内、外之间进行数据交换。
内部排序的基本操作:
对内部排序而言,其基本操作有两种:
- 比较两个关键字的大小;
- 存储位置的移动:从一个位置移动到另一个位置。
第一种操作是必不可少的;而第二种操作却不是必须的,取决于记录的存储方式,具体情况是:
(1)记录存储在一组连续地址的存储空间:记录之间的逻辑顺序关系是通过其物理存储位置的相邻来体现,记录的移动是必不可少的;
(2)记录采用链式存储方式:记录之间的逻辑顺序关系是通过结点中的指针来体现,排序过程仅需修改结点的指针,而不需要移动记录;
(3)记录存储在一组连续地址的存储空间:构造一个辅助表来保存各个记录的存放地址(指针):排序过程不需要移动记录,而仅需修改辅助表中的指针,排序后视具体情况决定是否调整记录的存储位置。
(1)比较适合记录数较少的情况;而(2)和(3)则适合记录数较少的情况。
为讨论方便,假设待排序的记录是以(1)的情况存储,且设排序是按升序排列的;关键字是一些可直接用比较运算符进行比较的类型。
而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。
常见的内部排序算法有:插入排序、希尔排序、选择排序、冒泡排序、归并排序、快速排序、堆排序、基数排序等。
1. 排序算法分析
1.1关于时间复杂度:
- 平方阶(O(n2))排序各类简单排序:直接插入、直接选择和冒泡排序;
- 线性对数阶(O(nlog2n))排序:快速排序、堆排序和归并排序;
- O(n1+k)排序,k是介于0和1之间的常数:希尔排序;
- 线性阶(O(n))排序:基数排序,此外还有桶,箱排序。
1.2关于稳定性:
- 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序;
- 不是稳定的排序算法:选择排序、快速排序、希尔排序、堆排序。
2.冒泡排序
冒泡排序的原理非常简单,它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。
2.1算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对第0个到第n-1个数据做同样的工作。这时,最大的数就“浮”到了数组最后的位置上。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
动画演示:
2.2 算法代码(基于python)
def bubble_sort(arry):
n = len(arry) #获得数组的长度
for i in range(n):
for j in range(1,n-i):
if arry[j-1] > arry[j] : #如果前者比后者大
arry[j-1],arry[j] = arry[j],arry[j-1] #则交换两者
print(arry)
return arry
arry = [5,4,7,9,2,1,6]
bubble_sort(arry)
#实验结果:1,2,4,5,6,7,9
流程图:
3. 选择排序
3.1 算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
动画演示:
3.2 算法代码(基于python)
def select_sort(ary):
n = len(ary)
for i in range(0,n):
min = i #最小元素下标标记
for j in range(i+1,n):
if ary[j] < ary[min] :
min = j #找到最小值的下标
ary[min],ary[i] = ary[i],ary[min] #交换两者
print(ary)
return ary
ary = [6,4,3,1,7,4,9,2]
select_sort(ary)
#实验结果:1,2,3,4,4,6,7,8
流程图:
4. 插入排序
插入排序:每一趟将一个待排序的记录,按照其关键字的大小插入到有序队列的合适位置里,直到全部插入完成。
4.1 算法步骤
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果被扫描的元素(已排序)大于新元素,将该元素后移一位
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
动画演示:
4.2 算法代码(基于python)
def insert_sort(ary):
n = len(ary)
for i in range(1,n):
if ary[i] < ary[i-1]:
temp = ary[i]
index = i #待插入的下标
for j in range(i-1,-1,-1): #从i-1 循环到 0 (包括0)
if ary[j] > temp :
ary[j+1] = ary[j]
index = j #记录待插入下标
else :
break
ary[index] = temp
print(ary)
return ary
ary = [1,4,5,0,8,2,7,3,9,0]
insert_sort(ary)
#实验结果:0,0,1,2,3,4,5,7,8,9
流程图:
5. 希尔排序
把待排序的数据元素分成若干个小组,对同一小组内的数据元素用直接插入法排序;小组的个数逐次减少;当完成了所有数据元素都在一个组内的排序后,排序过程结束。希尔排序又称作缩小增量排序。
5.1 算法步骤
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
动画演示:
5.2 算法代码(基于python)
def shell_sort(ary):
n = len(ary)
gap = round(n/2) #初始步长 , 用round四舍五入取整
while gap > 0 :
for i in range(gap,n): #每一列进行插入排序 , 从gap 到 n-1
temp = ary[i]
j = i
while ( j >= gap and ary[j-gap] > temp ): #插入排序
ary[j] = ary[j-gap]
j = j - gap
ary[j] = temp
gap = round(gap/2) #重新设置步长
print(ary)
return ary
ary = [1,5,7,2,9,4,3,1,0]
shell_sort(ary)
#实验结果:0,1,1,2,3,4,5,7,9
流程图:
6. 归并排序
归并排序主要是二路归并排序。
归并排序是采用分治法的一个非常典型的应用。归并
排序的思想就是先递归
分解数组,再合
并数组。
先考虑合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
6.1 算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
动画演示:
6.2 算法代码(基于python)
def merge_sort(ary):
if len(ary) <= 1 : return ary
num = int(len(ary)/2) #二分分解
left = merge_sort(ary[:num])
right = merge_sort(ary[num:])
return merge(left,right) #合并数组
def merge(left,right):
'''合并操作,
将两个有序数组left[]和right[]合并成一个大的有序数组'''
l,r = 0,0 #left与right数组的下标指针
result = []
while l<len(left) and r<len(right) :
if left[l] < right[r]:
result.append(left[l])
l += 1
else:
result.append(right[r])
r += 1
result += left[l:]
result += right[r:]
print(result)
return result
ary = [5,6,2,1,9,7,0,1,3]
merge_sort(ary)
#实验结果:0,1,1,2,3,5,6,7,9
流程图:
7. 快速排序
快速排序是一种二叉树结构的交换排序方法。
7.1 算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
动画演示:
7.2 算法代码(基于python)
def quick_sort(ary):
return qsort(ary,0,len(ary)-1)
def qsort(ary,left,right):
#快排函数,ary为待排序数组,left为待排序的左边界,right为右边界
if left >= right : return ary
key = ary[left] #取最左边的为基准数
lp = left #左指针
rp = right #右指针
while lp < rp :
while ary[rp] >= key and lp < rp :
rp -= 1
while ary[lp] <= key and lp < rp :
lp += 1
ary[lp],ary[rp] = ary[rp],ary[lp]
ary[left],ary[lp] = ary[lp],ary[left]
qsort(ary,left,lp-1)
qsort(ary,rp+1,right)
print(ary)
return ary
ary = [5,1,4,8,9,2,0,2,7,3]
quick_sort(ary)
#实验结果:0,1,2,3,4,5,7,8,9
流程图:
8. 堆排序
二叉堆具有以下性质:
- 父节点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
- 每个节点的左右子树都是一个二叉堆(都是最大堆或最小堆)。
8.1 算法步骤
- 构造最大堆(Build_Max_Heap):若数组下标范围为0~n,考虑到单独一个元素是大根堆,则从下标
n/2
开始的元素均为大根堆。于是只要从n/2-1
开始,向前依次构造大根堆,这样就能保证,构造到某个节点时,它的左右子树都已经是大根堆。 - 堆排序(HeapSort):由于堆是用数组模拟的。得到一个大根堆后,数组内部并不是有序的。因此需要将堆化数组有序化。思想是移除根节点,并做最大堆调整的递归运算。第一次将
heap[0]
与heap[n-1]
交换,再对heap[0...n-2]
做最大堆调整。第二次将heap[0]
与heap[n-2]
交换,再对heap[0...n-3]
做最大堆调整。重复该操作直至heap[0]
和heap[1]
交换。由于每次都是将最大的数并入到后面的有序区间,故操作完后整个数组就是有序的了。 - 最大堆调整(Max_Heapify):该方法是提供给上述两个过程调用的。目的是将堆的末端子节点作调整,使得子节点永远小于父节点 。
动画演示:
8.2 算法代码(基于python)
def heap_sort(ary) :
n = len(ary)
first = int(n/2-1) #最后一个非叶子节点
for start in range(first,-1,-1) : #构造大根堆
max_heapify(ary,start,n-1)
for end in range(n-1,0,-1): #堆排,将大根堆转换成有序数组
ary[end],ary[0] = ary[0],ary[end]
max_heapify(ary,0,end-1)
print(ary)
return ary
#最大堆调整:将堆的末端子节点作调整,使得子节点永远小于父节点
#start为当前需要调整最大堆的位置,end为调整边界
def max_heapify(ary,start,end):
root = start
while True :
child = root*2 +1 #调整节点的子节点
if child > end : break
if child+1 <= end and ary[child] < ary[child+1] :
child = child+1 #取较大的子节点
if ary[root] < ary[child] : #较大的子节点成为父节点
ary[root],ary[child] = ary[child],ary[root] #交换
root = child
else :
break
ary = [5,2,1,7,8,3,9,0,4]
heap_sort(ary)
#实验结果:0,1,2,3,4,5,7,8,9
流程图:
9. 计数排序
计数排序是一种非常快捷的稳定性强的排序方法,时间复杂度O(n+k),其中n为要排序的数的个数,k为要排序的数的组大值。
计数排序对一定量的整数排序时候的速度非常快,一般快于其他排序算法。但计数排序局限性比较大,只限于对整数进行排序。
计数排序是消耗空间发杂度来获取快捷的排序方法,其空间发展度为O(K)同理K为要排序的最大值。
9.1 算法步骤
- 花O(n)的时间扫描一下整个序列 A,获取最小值 min 和最大值 max
- 开辟一块新的空间创建新的数组 B,长度为 ( max - min + 1)
- 数组 B 中 index 的元素记录的值是 A 中某元素出现的次数
- 最后输出目标整数序列,具体的逻辑是遍历数组 B,输出相应元素以及对应的个数
动画演示:
9.2 算法代码(基于python)
def CountingSort(a, b, k):
c=[]
for i in range(k+1):
c.append(0)
for j in range(len(a)):
c[a[j]] = c[a[j]] + 1
for i in range(1, k+1):
c[i] = c[i] + c[i-1]
for j in range(len(a)-1, -1, -1):
b[c[a[j]]-1] = a[j]
c[a[j]] = c[a[j]] - 1
print(b)
a=[2, 5, 7, 8, 1, 3, 9, 4]
b=[None for i in range(len(a))]
CountingSort(a, b, max(a))
#运行结果:1,2,3,4,5,7,8,9
流程图:
10. 桶排序
桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。
为了使桶排序更加高效,我们需要做到这两点:
- 在额外空间充足的情况下,尽量增大桶的数量
- 使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中
10.1 算法步骤
- 设置一个定量的数组当作空桶子。
- 寻访序列,并且把项目一个一个放到对应的桶子去。
- 对每个不是空的桶子进行排序。
- 从不是空的桶子里把项目再放回原来的序列中。
动画演示:
10.2 算法代码(基于python)
def bucket_sort(the_list):
#设置全为0的数组
all_list = [0 for i in range(max(the_list)+1)]
last_list = []
for v in the_list:
if all_list[v]==0:
all_list[v] = 1
else:
all_list[v]+=1
for i,t_v in enumerate(all_list):
if t_v != 0:
for j in range(t_v):
last_list.append(i)
return last_list
if __name__ == '__main__':
the_list = [10, 1, 18, 35, 23, 12, 7, 5, 18, 17,19]
print("排序前:" + str(the_list))
print("排序后:" + str(bucket_sort(the_list)))
#排序前:[10, 1, 18, 35, 23, 12, 7, 5, 18, 17, 19]
#排序后:[1, 5, 7, 10, 12, 17, 18, 18, 19, 23, 35]
流程图:
11. 基数排序
基数排序又称为“桶子法”,从低位开始将待排序的数按照这一位的值放到相应的编号为0~9的桶中。等到低位排完得到一个子序列,再将这个序列按照次低位的大小进入相应的桶中,一直排到最高位为止,数组排序完成。
分为两类:
- 最低位优先法,简称LSD法:先从最低位开始排序,再对次低位排序,直到对最高位排序后得到一个有序序列。
- 第二类:最高位优先法,简称MSD法:先从最高位开始排序,再逐个对各分组按次高位进行子排序,循环直到最低位。
11.1 算法步骤
- 遍历序列找出最大的数(为的是确定最大的数是几位数);
- 开辟一个与数组大小相同的临时数组tmp;
- 用一个count数组统计原数组中某一位(从低位向高位统计)相同的数据出现的次数;
- 用一个start数组计算原数组中某一位(从最低位向最高位计算)相同数据出现的位置;
- 将桶中数据从小到大用tmp数组收集起来;
- 重复(3)(4)(5)直到所有位都被统计并计算过,用tmp收集起来;
- 将tmp数组拷回到原数组中;
动画演示:
11.2 算法代码(基于python)
def RadixSort(Lst):
#列表中的最大元素的位数
d = len(str(max(Lst)))
#0-9一共10个桶
for i in range(d):
BucketLst = [[] for k in range(10)]
for j in range(len(Lst)):
#10**i是关注意点,之前一直是10**d,debug才发现
BucketLst[Lst[j]//(10**i)%10].append(Lst[j])
Lst = [number for B in BucketLst for number in B]
return Lst
#列表中的最大元素的位数
Lst = [4,6,2,1,9,7,0,3,8]
print(RadixSort(Lst))
#实验结果:0, 1, 2, 3, 4, 6, 7, 8, 9
流程图: