最近因为找实习的需要,对常见的几大类排序算法做了基于python的实现,特地拿出来和大家分享,也欢迎看到的朋友如果发现里面有bug的话积极提出,共同进步
核心思想:每次将一个待排序的记录,按其关键字的大小插入到前面已经排好序的子序列中,直到全部记录插入完毕。每次插入,都能得到一个局部有序的子序列。
不同插入排序方法的区别:从局部有序序列中选择待排序记录的插入位置的方法不同
希尔排序的思想和以上两种插入排序算法都不一样:希尔排序先取一个小余 n n n的步长 d 1 d_{1} d1,将表中的全部记录分为 d 1 d_{1} d1组,分别对每个组进行直接插入排序;然后取第二个步长 d 2 < d 1 d_{2}
希尔提出的方法是:
d 1 = n / 2 d_{1}=n/2 d1=n/2
d i + 1 = ⌊ d i / 2 ⌋ d_{i+1}=\lfloor d_{i}/2 \rfloor di+1=⌊di/2⌋
空间复杂度为 O ( 1 ) , O(1), O(1),时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)
def straight_insertion_sort(nums, incremental=True):
nums.insert(0, 0) # 0位置不存元素,用作哨兵
nums_len = len(nums)
if incremental == True:
# 升序排列
for i in range(2, nums_len):
if nums[i] < nums[i-1]: # 如果需要插入
nums[0] = nums[i] # 在0位置插入“哨兵”
j = i-1
while nums[0] < nums[j]: # 找到插入的位置,并在比较的过程中每次都将元素后移,当当前元素小于等于待插入元素跳出
nums[j+1] = nums[j] # 每次将当前元素后移
j -= 1
nums[j+1] = nums[0] # 将待插入元素放入
print("排序后的升序序列为:", nums[1:])
else:
# 降序排列
for i in range(2, nums_len):
if nums[i] > nums[i-1]: # 如果需要插入
nums[0] = nums[i] # 在0位置插入“哨兵”
j = i-1
while nums[0] > nums[j]: # 找到插入的位置,并在比较的过程中每次都将元素后移,当当前元素大于等于待插入元素跳出。因为有哨兵的存在,肯定不会越界
nums[j+1] = nums[j] # 每次将当前元素后移
j -= 1
nums[j+1] = nums[0] # 将待插入元素放入
print("排序后的降序序列为:", nums[1:])
if __name__ == "__main__":
nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
straight_insertion_sort(nums, True)
空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)
def binary_insertion_sort(nums, incremental=True):
nums.insert(0, 0) # 0位置不存元素,用作哨兵
nums_len = len(nums)
if incremental == True:
# 升序排列
for i in range(2, nums_len):
if nums[i] < nums[i-1]: # 如果需要插入
nums[0] = nums[i] # 在0位置插入“哨兵”
# 在局部有序序列中折半查找寻找插入位置
low = 1
high = i - 1
while low <= high:
mid = (low + high) // 2
if nums[mid] <= nums[0]: # 当mid值小于等于待插入元素,low=mid+1
# 这里尤为要注意的就是相等时,应该是low = mid + 1,可以保证循环的结束条件是low = high
low = mid + 1
else:
high = mid - 1
for j in range(i-1, high, -1):
nums[j+1] = nums[j]
nums[high+1] = nums[0]
print("排序后的升序序列为:", nums[1:])
else:
# 降序排列
for i in range(2, nums_len):
if nums[i] > nums[i-1]: # 如果需要插入
nums[0] = nums[i] # 在0位置插入“哨兵”
# 在局部有序序列中折半查找寻找插入位置
low = 1
high = i - 1
while low <= high:
""""
这里注意low=high的时候还是得判断一下
"""
mid = (low + high) // 2
if nums[mid] >= nums[0]: # 当mid值大于等于待插入元素,low=mid+1
# 这里尤为要注意的就是相等时,应该是low = mid + 1,可以保证循环的结束条件是low = high
low = mid + 1
else:
high = mid - 1
for j in range(i-1, high, -1):
nums[j+1] = nums[j]
nums[high+1] = nums[0]
# print(nums[1:])
print("排序后的降序序列为:", nums[1:])
if __name__ == "__main__":
nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
binary_insertion_sort(nums, True)
# binary_insertion_sort(nums, False)
空间复杂度为 O ( 1 ) O(1) O(1),最坏情况下的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)
希尔提出的方法是:
d 1 = n / 2 d_{1}=n/2 d1=n/2
d i + 1 = ⌊ d i / 2 ⌋ d_{i+1}=\lfloor d_{i}/2 \rfloor di+1=⌊di/2⌋
def shell_sort(nums, incremental=True):
nums.insert(0, 0) # 0位置不存元素也不用做哨兵,只是用来暂存元素的,因为希尔排序中是分组排序的,不是每次都能遍历到0元素位置
d_i = (len(nums)-1) // 2
if incremental == True:
while d_i >= 1: # 对于每一个步长
for i in range(1, d_i+1): # 确定每一组的起始元素,每两个元素之间相差d_i
# 每一个组中的元素下标为:i, i+d_i, i+2*d_i, i+3*d_i, ......
for j in range(i+d_i, len(nums), d_i): # 对每一组做直接选择排序
if nums[j] < nums[j-d_i]:
nums[0] = nums[j] # 暂存当前元素
k = j - d_i
#print("k={},len(nums)={},nums[k]={}, nums[0]={}".format(k, len(nums), nums[k], nums[0]))
while k > 0 and nums[k] > nums[0]: # 这里因为没有哨兵,所以要注意防止越界
nums[k+d_i] = nums[k]
k -= d_i
nums[k+d_i] = nums[0]
d_i = d_i // 2
print("排序后的升序序列为:", nums[1:])
else:
while d_i >= 1: # 对于每一个步长
for i in range(1, d_i+1): # 确定每一组的起始元素,每两个元素之间相差d_i
# 每一个组中的元素下标为:i, i+d_i, i+2*d_i, i+3*d_i, ......
for j in range(i+d_i, len(nums), d_i): # 对每一组做直接选择排序
if nums[j] > nums[j-d_i]:
nums[0] = nums[j] # 暂存当前元素
k = j - d_i
while k > 0 and nums[k] < nums[0]:
nums[k+d_i] = nums[k]
k -= d_i
nums[k+d_i] = nums[0]
d_i = d_i // 2
print("排序后的降序序列为:", nums[1:])
if __name__ == "__main__":
nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
# shell_sort(nums, True)
shell_sort(nums, False)
核心思想:根据序列中两个关键字的比较结果来对换关键字在序列中的位置,每轮交换结束后,都能确定一个元素的最终位置。
交换思想的排序算法主要包括两种
空间复杂度为 O ( 1 ) O(1) O(1)
最好的时间复杂度为 O ( n ) O(n) O(n),最差的时间复杂度为 O ( n 2 ) O(n^{2}) O(n2),平均时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)
def bubble_sort(nums, incremental=True):
if incremental == True:
for i in range(len(nums)): # n趟冒泡
flag = 0 # 判断本轮冒泡是否发生了交换,如果没有发生交换,说明整个序列有序,可以直接跳出,冒泡排序是不需要设置哨兵的,因为肯定不会越界
for j in range(len(nums)-1, i, -1):
if nums[j] < nums[j-1]:
tmp = nums[j]
nums[j] = nums[j-1]
nums[j-1] = tmp
flag = 1
if flag == 0:
break
print("排序后的升序序列为:", nums)
else:
for i in range(len(nums)): # n趟冒泡
flag = 0 # 判断本轮冒泡是否发生了交换,如果没有发生交换,说明整个序列有序,可以直接跳出,冒泡排序是不需要设置哨兵的,因为肯定不会越界
for j in range(len(nums)-1, i, -1):
if nums[j] > nums[j-1]:
tmp = nums[j]
nums[j] = nums[j-1]
nums[j-1] = tmp
flag = 1
if flag == 0:
break
print("排序后的降序序列为:", nums)
if __name__ == "__main__":
nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
# bubble_sort(nums, True)
bubble_sort(nums, False)
最好情况下栈的深度为 O ( n ) O(n) O(n),最差情况下栈的深度为 O ( l o g 2 n ) O(log_{2}n) O(log2n),平均情况下的时间复杂度为 O ( l o g 2 n ) O(log_{2}n) O(log2n)
最坏情况下每次取的pivot为最右边或者最左边的值,时间复杂度为 O ( n 2 ) O(n^{2}) O(n2)。最好的情况下,每次pivot去的都是中间值,时间复杂度为 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n)
# flag = 0
def partition(nums, low, high):
if low > high: # 如果递归结束条件写在这,递归结束条件一定是low > high
return
tmp_low = low
tmp_high = high
#print("1---{}---{}".format(low, high))
pivot = nums[low]
while low < high:
# 先从右边找出第一个小于pivot的值
while low < high and nums[high] >= pivot:
high -= 1
nums[low] = nums[high] # 将high位置的值赋给low位置的值
# 再从左边找到第一个大于pivot的值
while low < high and nums[low] < pivot:
low += 1
nums[high] = nums[low] # 将low位置的值赋给high位置的值
nums[low] = pivot
# 确定了pivot的位置后,分别对左右两部分进行相同操作,每次都能确定一个pivot的最终位置
# print("tmp_low={}, tmp_high={}, low={}".format(tmp_low, tmp_high, low))
partition(nums, tmp_low, low-1)
partition(nums, low+1, tmp_high)
def quick_sort(nums, incremental=True):
if incremental == True:
#print("2---", len(nums))
partition(nums, 0, len(nums)-1)
print(nums)
if __name__ == "__main__":
nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
quick_sort(nums)
核心思想:每一趟(比如第 i i i趟)在后面 n − i + 1 n-i+1 n−i+1个待排序的元素中选择出最小的元素,作为有序子序列的第 i i i个元素,直到第 n − 1 n-1 n−1趟做完,待排序元素只剩下一个,就不再选了。
选择排序主要两种:
简单选择排序:假设待排序表为 L [ 1 , 2 , ⋯ , n ] L[1, 2, \cdots, n] L[1,2,⋯,n],第 i i i趟排序从 L [ i , i + 1 , ⋯ , n ] L[i, i+1, \cdots, n] L[i,i+1,⋯,n]中选择出最小的元素与 L [ i ] L[i] L[i]交换, L [ i ] L[i] L[i]中的元素最终位置是确定的。
堆排序:在排序过程中,将 L [ 1 , 2 , ⋯ , n ] L[1, 2, \cdots, n] L[1,2,⋯,n]看做是一棵完全二叉树的顺序存储结构,利用完全二叉树的顺序存储结构中的双亲节点和孩子节点的下标关系,从中选择出关键字最大(或最小)的元素
空间复杂度为 O ( 1 ) O(1) O(1)
最好和最坏的时间复杂度都是 O ( n 2 ) O(n^{2}) O(n2)
def simple_selection_sort(nums, incremental=True):
if incremental == True:
for i in range(len(nums)): # 每次确定一个位置的值
min_index = i
for j in range(i+1, len(nums)): # 从剩余的元素中得到其中的最小值
if nums[j] < nums[min_index]:
min_index = j
if min_index != i:
tmp = nums[i]
nums[i] = nums[min_index]
nums[min_index] = tmp
else:
for i in range(len(nums)):
max_index = i
for j in range(i+1, len(nums)):
if nums[j] > nums[max_index]:
max_index = j
if max_index != i:
tmp = nums[i]
nums[i] = nums[max_index]
nums[max_index] = tmp
print(nums)
if __name__ == "__main__":
nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
#simple_selection_sort(nums)
simple_selection_sort(nums, False)
空间复杂度为 O ( 1 ) O(1) O(1)
建堆的时间复杂度为 O ( n ) O(n) O(n),每次向下调整的时间复杂度为 O ( h ) O(h) O(h),最好、最坏和平均时间复杂度都为 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n)
基于完全二叉树的顺序存储结构的大根堆和小根堆:
以小根堆为例:
堆的构建和维护主要涉及向上调整和向下调整两个过程
小根堆的初始堆构建过程:
从最后一个叶子节点的父节点开始,逐个节点向上调整,对每个节点:
1.1 判断当前节点与孩子节点的大小,如果当前节点值最小,则不做向下调整,如果当前节点值不是最小,则将当前节点值和孩子节点中的最小值调换
1.2 从上一步调换过后的孩子节点开始向下调整,返回1.1步递归直到结束
在构建了小根堆之后,我们每次取出堆顶元素,即为当前最小值,此时涉及到堆的删除操作,删除堆顶元素后,先将堆的最后一个元素填充到堆顶,再从堆顶开始做向下调整重新构建小根堆。注意此时不需要做向上调整,只需要做向下调整。
小根堆的插入操作:先将元素插入到堆的末端,再从堆的末端开始做向上调整。注意此时因为是插入到已经构建好的小根堆中,所以不需要向下调整,只需要做向上调整。
因为每次都会弹出堆顶元素,再将最末位的元素弹出填充到栈顶,所以我们可以将每次弹出的栈顶元素放到堆的末尾,以填充空出来的栈末尾的位置。在这种思路下,得到升序序列需要借助大根堆,得到降序序列需要借助小根堆
堆排序在取待排序序列的前n大或者前n小的元素时是非常有效的,尤其是在数据量较大(可能内存无法同时装下所有元素时)时,只需要维持一个大小为n的堆即可。
def adjust_up(nums, index): # 小根堆的向上调整过程。向上调整过程在插入时用
while index >= 1:
if index // 2 >= 1:
if nums[index//2] > nums[index]:
tmp = nums[index]
nums[index] = nums[index//2]
nums[index//2] = tmp
else: # 如果父亲节点更小,则不需要再继续向上调整
break
index = index // 2
def adjust_down(nums, index, n):
while True: # 对每一个节点做向下调整
min_index = index
if index * 2 <= n: # 当前节点的左孩子节点存在,判断是否比父亲节点小
if nums[index*2] < nums[min_index]:
min_index = index * 2
if index * 2 + 1 <= n: # 当前节点的右孩子存在,判断是否比父亲节点小
if nums[index*2+1] < nums[min_index]:
min_index = index * 2 + 1
if min_index != index: # 说明孩子节点中有比父亲节点小的,则交换两个的值,再继续从最小的孩子节点向下调整
tmp = nums[min_index]
nums[min_index] = nums[index]
nums[index] = tmp
index = min_index
else: # 如果两个孩子节点都没有比当前节点小的,则本次向下调整结束
break
def build_min_heap(nums, last_leaf):
node_index = last_leaf // 2
while node_index >= 1: # 从最后一个叶节点的父亲节点开始遍历这个节点之前的所有点
adjust_down(nums, node_index, last_leaf)
node_index -= 1 # 向上对另一个元素进行调整
print("构建的初始小根堆为:", nums[1:])
def heap_sort(nums, incremental=True):
# 注意list的索引是从0开始,为了更好的利用下标进行索引,在0元素位置插入一个元素,使得真正的待排序序列从1开始
# 此时,当父节点下标为i时,它的两个孩子节点的下标为2*i和2*i+1
# 当孩子节点的下标为i时,其父节点的下标为i//2
nums.insert(0, 0)
last_leaf = len(nums) - 1
build_min_heap(nums, last_leaf) # 构建初始小根堆
# 每次弹出堆顶元素,然后用最后一个元素填充到堆顶,将弹出的堆顶元素存放到末尾元素位置,从堆顶做一次向下调整
for _ in range(len(nums)-2):
tmp = nums[1]
nums[1] = nums[last_leaf]
nums[last_leaf] = tmp
last_leaf -= 1
adjust_down(nums, 1, last_leaf)
print(nums[1:])
if __name__ == "__main__":
nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
heap_sort(nums, False)
核心思想:"归并"的含义是将两个或两个以上的有序表合并成一个新的有序表。假定待排序表有 n n n个记录,则可以看做有 n n n个长度为1的子表,然后两两进行归并,得到 ⌈ n / 2 ⌉ \lceil n/2 \rceil ⌈n/2⌉个长度为2的有序子表;继续归并,直到最后得到长度为 n n n的有序表。
空间复杂度为 O ( n ) O(n) O(n),因为只需要开一个全局的和待排序序列等长的辅助数组就行
时间复杂度为 O ( n l o g 2 n ) O(nlog_{2}n) O(nlog2n)
def merge(nums, low, mid, high):
# 先把两个有序子序列放入辅助列表中
tmp_1 = nums[low:mid+1]
tmp_2 = nums[mid+1:high+1]
i_1 = 0
i_2 = 0
i = low
while i_1 < len(tmp_1) and i_2 < len(tmp_2):
if tmp_1[i_1] < tmp_2[i_2]:
nums[i] = tmp_1[i_1]
i_1 +=1
i += 1
else:
nums[i] = tmp_2[i_2]
i_2 +=1
i += 1
# 如果还有一个序列没有被加入
if i_1 < len(tmp_1):
nums[i: high+1] = tmp_1[i_1: ]
elif i_2 < len(tmp_2):
nums[i: high+1] = tmp_2[i_2: ]
def merge_sort(nums, low, high):
if low < high:
mid = (low + high) // 2
merge_sort(nums, low, mid) # 对左侧子序列进行递归排序
merge_sort(nums, mid+1, high) # 对右侧子序列进行递归排序
# 左右两个子序列都完成排序后,将这两个子序列合并
merge(nums, low, mid, high)
else:
return
if __name__ == "__main__":
nums = [4, 2, 6, 8, 10, 3, 6, 7, 1, 0, 9, 8]
merge_sort(nums, 0, len(nums)-1)
print(nums)