什么是排序?
排序是按关键字的非递减或者非递增顺序对一组记录重新进行排列的操作。
(1)排序的稳定性
- 通俗地讲,就是排序前 Ri 在 Rj前面,排序后Ri仍领先于Rj, 则说明是稳定的。(Ri<Rj)
(2)内部排序与外部排序
(3)内部排序方法的分类
(4)待排序记录的存储方式
- 一般为顺序存储结构
- 或者是链式存储结构
(5)排序算法效率的评价指标
一.插入排序
(1)直接插入排序
"""
直接插入排序:
采用将r[i]与r[i-1], r[i-2],...,r[1],从后向前的进行比较,也就是采用顺序查找法,查找当前元素应该插入的位置。
特点:
1. 稳定排序
2. 算法简便且容易实现
3. 适用于链式存储结构。
4. 更适用于初始记录基本有序的情况。
5. 初始记录无序,n(排序元素个数) 过大时,不宜采用。
时间复杂度为:O(n^2)
空间复杂度为: O(1) 需要一个辅助空间作为元素交换(python不需要辅助空间)
"""
arr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
def InsertSort(l):
count = len(l)
for i in range(1, count):
j = i
temp = l[i]
while j > 0 and temp < l[j-1]: # 当j = 1 时,就已经是 l[1] 和 l[0] 进行比较了,注意这里的条件不能是>= 0
# l[j], l[j-1] = l[j-1], l[j]
l[j] = l[j-1]
j -= 1
l[j] = temp
InsertSort(arr)
print(arr)
(2)折半插入排序
"""
折半插入排序: 不同于 直接插入排序的顺序查找位置,采用“折半查找”,就叫做折半插入排序。
特点:
1. 稳定排序。
2. 要进行折半查找,所以只能用于顺序结构,不能用于链式结构。
3. 适合初始记录无序,n较大的情况。
时间复杂度:O(n^2)
空间复杂度: O(1)
平均时间性能要优于直接插入,但是我觉得, 折半和顺序直接插入都一样,1.都是要找到插入位置, 2.移动元素插入进去
不管咋样查找插入位置,都要移动。没啥大的差别。 直接插入是边判断边移动, 折半是 判断好了,移动完成再插入
"""
arr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
def BInsertSort(l):
n = len(l)
for i in range(1, n):
temp = l[i]
low = 0
high = i-1
while low <= high: # 循环判断出插入位置: 这里很巧妙,想了很久! 不论怎样最后 low 和 high 都会相等
mid = int((low + high)/2) # 指向最后一个比较元素, 且无论比较如何,插入位置都是 high + 1 (牛!)
if l[i] < l[mid]:
high = mid - 1
else:
low = mid + 1
for j in range(i-1, high+1-1, -1): # 倒序 ,知道插入位置后, 一个个的把位置前的数字后移一位
l[j+1] = l[j]
l[high+1] = temp # 插入该元素
BInsertSort(arr)
print(arr)
(3)希尔排序
"""
希尔排序(缩小增量排序):直接插入排序,当待排序的记录个数较少且待排序序列的关键字基本有序时,效率高.
希尔排序针对 “减少记录个数”, “序列基本有序” 两个方面对直接插入排序进行改进。
实现算法:采用分组插入的方法,先将整个的待排序记录序列分割成几组,从而减少曹郁直接插入排序的数据量,对每组分别进行直接插入排序,
然后增加每组的数据量,重新分组。这样当经过几次分组排序后,整个序列中的记录“基本有序”时,在对全体数据进行依次直接插入排序。
特点:
1. 不稳定的排序(记录跳跃式地移动)
2. 只能用于顺序结构
3. 最后一个增量必须为 1,才能排序成功。
4. 记录总的比较次数和移动次数,都要比直接插入排序要少, n越大越明显。适合初始数据无序, n较大的情况。
时间复杂度: O(n^ 3/2), 科学计算某些情况下,比较合适移动次数 大概为 n^1.3, n 趋近无穷大时为: n(log2 n)^2
空间复杂度: O(1) , 只需要一个辅助空间
"""
arr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
def ShellInsert(l, dk):
""" 执行dk间距的排序 """
n = len(l)
for i in range(dk, n): # 这里这样写省略了一层循环,非常巧妙。 因为 dk+1, dk+2, dk+3 会分为以这三个开头的三个小列表,分别排序。
temp = l[i] # 但是这里发现,不用多一层循环分开排序这三小个,一层大循环就会做到这些功能,读者可以自行模拟该过程!很神奇
j = i
while j>=dk and temp < l[j-dk]: # 这里 j>= dk, 实际上当dk=1时,也就是直接插入排序,就相当于是 j >= 1 ,等价于j>0(与直接插入一致),
l[j] = l[j-dk] # 解决的是计算插入位置小于0的情况比如: 下面注释讲解
j = j-dk
l[j] = temp
def Shellsort(l, dk):
""" 指定dk值, 逆序dk -- 1去执行希尔排序 """
for k in range(dk, 0, -1): # 逆序
ShellInsert(l, k)
Shellsort(arr, 3)
print(arr)
"""
当执行 ShellInsert(arr, 3)时,读者可以自己一步步走这个过程。当执行最后一趟循环,i== 8时,
arr = [17, 26, 31, 44, 55, 93, 54, 77, 31, 20]
很明显 20 要和 93, 31 进行比较,但 如果 j >0 而不是 j >= dk, 得到最后 j 值计算到 31的位置时等于 2,
还要去 j-dk, 就得到了错误的下标 -1, 所以这里是 j >= dk, 很细节!
"""
交换排序
(1)冒泡排序
"""
冒泡排序特点
1. 稳定排序
2. 可用于链式存储结构
3. 移动记录次数较多,算法平均时间性能比直接插入排序差。
当n较大,初始记录无序时,此算法不宜采用。
时间复杂度:O(n**2)
空间复杂度:O(1) 只要一个辅助空间,做交换用
"""
a = [23, 45, 12, 67, 44, 78, 2, 99]
def bubbleSort(arr):
""" 最基础版本,任何数组,都要全部进行比较 """
n = len(arr)
for i in range(n):
for j in range(0, n - i -1):
if arr[j] > arr[j + 1]:
arr[j], arr[j + 1] = arr[j + 1], arr[j]
def plusBubbleSort(arr):
""" 升级版,提前判断出数组已经排序好了 """
n = len(arr)
flag = 1 # 当不等于1 时,说明没有做交换,已经排好顺序
while n != 0 and flag == 1:
flag = 0
for j in range(n - 1):
if arr[j] > arr[j + 1]:
flag = 1
arr[j], arr[j + 1] = arr[j + 1], arr[j]
n -= 1
plusBubbleSort(a)
print(a)
# 两个for 循环版本的
def bubbleSort1(arr):
""" 最基础版本,任何数组,都要全部进行比较 """
n = len(arr)
flag = 1
for i in range(n):
flag = 0
for j in range(0, n - i -1):
if arr[j] > arr[j + 1]:
flag = 1
arr[j], arr[j + 1] = arr[j + 1], arr[j]
if flag == 0:
break
(2)快速排序
"""
快速排序(由冒泡排序改进而来, 能通过两个不相邻的记录的一次交换,消除多个逆序,加快排序速度)
1. 不稳定的排序。(记录非顺次的移动导致排序方法是不稳定的)
2. 排序过程中需要定义上下界,不适合用于顺序结构,很难用于链式结构。
3. 适合排序 元素(n)数量较大的情况, 在平均情况下,它是内部排序方法中速度最快一种。
适合初始记录无序,n较大的情况
平均时间复杂度:O(nlog2 n), 最坏情况是 O(n)
空间复杂度: 最好情况下是 O(log2 n),最坏情况下是O(n)
递归的快速排序,执行时需要用一个栈来存放相应的数据,最大递归调用次数与递归树的深度一致。
递归查找用树表示的是: 递归树。
"""
arr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
def Partition(l, low, height):
pivotkey = l[low]
while low < height:
while low < height and l[height] >= pivotkey:
height -= 1
l[low] = l[height]
# low += 1
while low < height and l[low] < pivotkey:
low += 1
l[height] = l[low]
# height -= 1 # 按照正常的思路,应该执行这两个 low += 1, height -= 1 但是关键就错在,这两个值,会使low height指针重合后,再次发生偏移。 因为他们一定执行,不受限制。 所以不能加这两个语句,让他们在while 内部判断即可
l[low] = pivotkey
return low
def Qsort(l, low, height):
if low < height:
p = Partition(l, low, height)
Qsort(l, 0, p-1)
Qsort(l, p+1, height)
def QuckSort(l):
Qsort(l, 0, len(arr) - 1)
QuckSort(arr)
print(arr)
快速排序-分区逻辑:
二向切分的逻辑应用(快慢指针)
- 和快速排序中 pivotkey的快慢指针的思路是如出一辙。(前面小于pivotkey,中间大于pivotkey,后面还未护理到。只是判断条件不一样了)
三路快排的分区逻辑
- 对撞指针,比上面的代码实现好在。 处理了 分区去 有 等于pivotkey的区间。 处理时直接省略过去不用出里了。
- 实现
- 应用:75号算法题:颜色分类
选择排序
(1)简单选择排序
# 简单选择排序
"""
# 简单选择排序
1. 稳定的排序
2. 可用于链式存储结构
3. 移动记录次数较少,当每次占用的空间较多时,此方法比直接插入排序快。
与冒泡排序一样:
时间复杂度: O(n^2)
空间复杂度: O(1) 只需要一个辅助空间(python里辅助空间都是不需要的)
"""
arr = [54, 26, 93, 17, 77, 31, 44, 55, 20]
def simple_Quicksort(arr):
length = len(arr)
for i in range(length - 1):
min = arr[i] #记录下标即可, 这里采用有定义一个 min,看的比较清晰
index = i
for j in range(i+1, length):
if min > arr[j]:
min = arr[j]
index = j
arr[i], arr[index] = arr[index], arr[i]
# simple_Quicksort(arr)
(2)堆排序
# 树形选择排序
"""
树形选择排序:
在简单选择排序的基础上进行改进:
简单选择排序,在n个之中选择最小的, 至少要进行n-1次比较,
当继续在剩余的n-1个关键字中选择最小值, 未必非要进行n-2次比较,
若利用前面的n-1次比较所得的信息,则可以减少以后各趟选择排序中所用的比较次数,
类似于 体育比赛中的锦标赛。
树排序尚且有 辅助存储空间较多, “最大值“进行多于的比较等缺点
"""
# 堆排序
"""
堆排序:
是一种树形选择结构,将待排序的记录r[1..n] 看成是一颗完全二叉树的顺序存储结构,
利用完全二叉树中双亲节点和孩子节点的内在关系,在当前的无序的序列中选择关键字最大(或者最小)的记录。
实际上,堆就是 满足如下性质的二叉树:树中所有非终端节点的值均不大于(或者不小于)左,右孩子的值。
(二叉树和完全二叉树的性质,参考课本119页!)
大根堆,小根堆:堆顶元素(就是完全二叉数的根节点),比为n个元素中的最大值(大根堆),或者最小值(小根堆)。
特点:
1. 算法是不稳定排序
2. 只能用于顺序结构,不能用于链式结构
3. 记录数较高时,有优势。 相对于快速排序的最坏时间复杂度O(n^2),是一个优点。
时间复杂度:最坏时间复杂度:O(nlog2 n) (平均时间复杂度接近最坏时间复杂度)
运行时间主要耗费在 建立初堆和调整堆时进行的反复“筛选”上。
空间复杂度:O(1) 只需要一个辅助空间( python交换两个元素,也不需要辅助空间)
"""
# 例如,大根堆的排序过程如下
"""
(1) 按照按堆的定义,将待排序的序列 r[1..n]调整为大根堆(这个过程就是建初堆),交换r[1] 和 r[n],则r[n]为关键字最大的序列
(2) 将r[1..n-1] 重新调整为堆,交换r[1] 和 r[n-1], 则r[n-1]为关键字的次大的记录。
(3) 循环 n-1次, 直到交换了r[1] 和 r[2] 为止,得到了一个非递减的有序序列r[1..n]
所以实现堆排序,主要考虑两个问题:
(1) 建初堆:如何将一个无序序列建成一个堆?
(2) 调整堆: 去掉堆顶元素,在堆顶元素改变之后,如何调整剩下的元素成为一个新堆?
"""
a = [49, 97, 65, 49, 76, 13, 27, 38]
# 筛选法
def HeadAdjust(l, root_index, m):
""" 筛选法调整大根堆
:param l: 列表
:param root_index: 根的索引
:param m: 列表长度-1, 就是最后一位元素的下标值
"""
rc = l[root_index]
i = root_index*2 + 1
while i <= m: # 这里也必须是 <=, 试想当只有两个元素的时候, i = 2,正好 i=m, 执行。
if i<m and l[i]<l[i+1]: # 这里的 i<m, 就是用来判断,当最后叶子节点为1的时候,不会去算右节点 l[i + 1], 也就不会超出列表长度了
i += 1
if rc > l[i]:
break
l[root_index] = l[i]
root_index = i
i = i*2 + 1
l[root_index] = rc
"""
给出C语言版本的,真的是简洁,for循环代替递归,变量复用~ 简直完美! python for range循环我觉得不容易实现,才改成while循环
void HedpAdjust(SqList &L, int s, int m)
{
// 假设r[s+1..m],已经是大根堆, 将r[s..m]调整为以r[s]为根的大根堆
rc = L.r[s];
for(j=2*s; j<=m; j*=2) // 沿着key较大的孩子节点向下筛选
{
if(j<m && L.r[j].key < L.r[j+1].key) ++j; // j为key较大的记录下标
if(rc.key>= L.r[j].key) break;
L.r[s] = L.r[j]; s=j; // 解决递归问题, s重新赋值为 j,开始新的计算
}
L.r[s] = rc // 插入
}
与python版本的区别是 二叉树的性质:
c语言实现,下标从1开始, 与子节点的关系是: i(根) , 2i(左), 2i +1(右)
python下标从零开始: 与子节点的关系是: i(根) , 2i + 1(左), 2i + 2(右)
其他全部相同,就是改个下标~
"""
# HeadAdjust(a, 0, 7)
# 建初堆:
""" 只要是抓住一条性质,完全二叉树,序号大于 n/2 的节点都是叶子, 我们对[1..n/2]的节点依次调用 上面的筛选法,建立初堆 """
b = [49, 38, 65, 97, 76, 13, 27, 49]
def CreateHeap(l):
""" 建立初堆 """
n = len(l)
for i in range(int(n/2)-1, -1, -1): # 逆序, int(n/2)-1:第一个元素的下标, -1,开区间,为-1才能取到0
HeadAdjust(l, i, n-1)
# CreateHeap(b)
# 堆排序: 就是先进行 1.建初堆,2.进行调整堆
def HeapSort(l):
CreateHeap(l) # 建成大根堆
# 将根元素(最大的元素),与最后一个元素互换位置,然后重新调整,找出次最大的,依次进行
n = len(l)
for i in range(n-1, 0, -1):
l[0], l[i] = l[i], l[0]
HeadAdjust(l, 0, i-1)
HeapSort(b)
print(b)
计数排序
计数排序是一个非基于比较的排序算法,该算法于1954年由 Harold H. Seward 提出。它的优势在于在对一定范围内的整数排序时,它的复杂度为Ο(n+k)(其中k是整数的范围),快于任何比较排序算法。 [1] 当然这是一种牺牲空间换取时间的做法,而且当O(k)>O(n*log(n))的时候其效率反而不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n)), 如归并排序,堆排序)
- 下面可以看一道使用计数排序解决的算法题:力扣1365号. 有多少小于当前数字的数字
- 解法如下:基数排序就是利用所有元素在一定范围内。 可以是用数组索引表示出每一位,然后数组索引对应的元素,表示该值出现了多少次,计数。
class Solution:
def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]:
"""
基数排序解法
"""
# 数值大小是 0-100, 我们生成101长度的数组,计算每个元素出现的次数
count = [0] * 101
for num in nums:
count[num] += 1
# 再次当前位置的元素前面有多少小于它的元素
for i in range(1, 101):
count[i] += count[i - 1]
rst = [0]*len(nums)
# 返回对应的计数数组
for i, num in enumerate(nums):
rst[i] = count[num - 1] if num > 0 else 0
return rst
归并排序
- 归并排序,也是递归的思想,还有非递归的方式。
"""
归并排序:将两个或者两个以上的有序表合并成一个有序表的过程。
2-路归并: 将两个有序表合并成一个有序表的过程。最为简单常用。
"""
arr = [3, 6, 9, 4, 7, 13, 16]
a = [0]*9
arr1 = [54, 26, 93, 17, 77, 31, 44, 55, 20]
# 1. 先实现两个 有序序列的 归并,这里设两个有序表分别存在同一列表中l[low..mid], [mid..high]。
def Merge(l, t, low, mid, high):
"""
:param l:排序列表(存放两个有序表)
:param t:将l中的记录,由小到大的并入其中
:param low: 第一个有序表开始位置(l 首元素)
:param mid: 第二个有序表开始位置(l 中间位置)
:param high: 第二个表结尾 (l结尾)
实现思路: 将左右子数组拷贝到 t(临时数组中),将归并后的正确结果,还放回去到l对应的位置
"""
# 拷贝l 对应的元素到 t里来
for i in range(low, high+1):
t[i] = l[i]
i = k = low
j = mid + 1
# 合并之后的排序,重新放回 l
while i <= mid and j <= high:
if t[i] < t[j]:
l[k] = t[i]
i += 1
else:
l[k] = t[j]
j += 1
k += 1
# 将 两个有序表,其中的剩余元素,添加进 l对应的尾部
""" 实际上这里可以使用extend()方法,或者 列表的+操作代替,但是算法就要原汁原味,我们不动用函数,只用append函数想列表里添加元素"""
while i <= mid:
l[k] = t[i]
i += 1
k += 1
while j <= high:
l[k] = t[j]
j += 1
k += 1
# print(arr)
# Merge(arr, a, 0, 3, 6)
# print(arr)
# print(a)
def Msort(l, t, low, high):
""" 归并排序 """
# 当只有一个元素时,一定要返回上一层
if(low >= high): return
mid = (low+high)//2 # 注意这里 low-->mid 和 mid + 1 -->high 才是平分! 我之前写的 low --> mid - 1, mid --> high 不是平分会报错!细节问题
Msort(l, t, low, mid)
Msort(l, t, mid+1, high)
# 最小子问题,左右各有一个元素,执行排序合并
Merge(l, t, low, mid, high)
def MergeSort(l, t):
Msort(l, t, 0, 8)
MergeSort(arr1, a)
print(arr1)
基数排序
- 来自百度百科的讲解,很详细了
- https://baike.baidu.com/item/%E5%9F%BA%E6%95%B0%E6%8E%92%E5%BA%8F/7875498?fr=aladdin#5_6
这里给出完全二叉树相关的相关性质:
- 特性:对堆理解有用
先到这里了,归并算法好像还有点问题~ 需要后续再思考,调整一下补充一下,这里并没有列出基数排序(脑袋一次性接收不了这么多了~~~~)
补充:排序算法的选择与比较
(1)排序算法的比较
(2)排序算法的选择
(3)如何写一个通用的排序算法
- 如java中的内置方法
sort()
就是一个通用排序。
对于基本类型的比较:比如int
对于引用类型,有两种方式实现
python中也是一样
可以实现 __eq 和 __lt 等方法, 使对象变成可比较的
Java引用类型的通用排序
- 顺序
这里不再使用 快速排序,因为快速排序是 不稳定排序!
, 可能影响最终排序结果。