在探讨列表的排序之前,我们先来了解一下列表的查找问题吧!
一、列表的查找
列表查找:顾名思义就是从列表中查找指定元素
函数要求:
输入列表、待查找元素,输出元素下标或未查找到元素
两种方式:
顺序查找:从列表第一个元素开始,顺序进行搜索,直到找到为止。时间复杂度是O(n)
二分查找:从有序列表的候选区data[0:n]开始,通过对待查找的值与候选区中间值的比较,可以使候选区减少一半。时间复杂度是O(logn)
顺序查找代码示例:
def linear_search(li, val):
for index in range(len(li)):
if li[index] == val:
return index
else:
return None
二分查找代码示例:
def bin_search(li, val):
low = 0
high = len(li) - 1
while low <= high:
mid = (low + high) // 2
if li[mid] == val:
return mid
elif li[mid] > val:
high = mid - 1
else:
low = mid + 1
递归版本的二分查找
def bin_search_revc(li, val, low, high):
if low <= high:
mid = (low + high) // 2
if li[mid] == val:
return mid
elif li[mid] > val:
return bin_search_revc(li, val, low, mid - 1)
else:
return bin_search_revc(li, val, mid + 1, high)
else:
return None
二、列表排序---冒泡排序、选择排序、插入排序
列表排序的目的:
将无序列表变为有序列表应用场景:
1.各种表单,排行榜,热搜榜等
2.各种表格,数据处理
3.给二分查找有序结果
4.其他运算列表排序的两个关键词:有序区和无序区
三种基本的排序方式:
1.冒泡排序:列表每两个相邻的数比较大小,如果前边的比后边的大,那就互换位置。就像冒泡。
2.选择排序:一趟遍历记录最小的数,放到第一个位置,再一趟遍历记录剩余列表中最小的数,继续放置
3.插入排序:列表被分为有序区和无序区两个部分。最初有序区只有一个元素。每次从无序区选择一个元素,插入到有序区的位置,直到无序区变空。
冒泡排序动图演示:
冒泡排序代码:时间复杂度O(n²)
两层for循环,第一层循环控制总趟数,第二层循环无序区,比较大小,交换位置
def bubble_sort(li):
# i表示第i趟,共n-1趟
for i in range(len(li) - 1):
# 第i趟 无序区范围 0~n-i-1
flag = False
for j in range(len(li) - i - 1):
if li[j] > li[j + 1]:
li[j], li[j + 1] = li[j + 1], li[j]
flag = True
# 如果一趟下来列表的顺序没有任何改变,
# 证明已经是有序的了,直接结束函数
if not flag:
return
选择排序代码:O(n²)
两层for循环,第一层循环控制总趟数,默认最小值是无序区第一个数,第二层循环无序区,寻找并记录最小值位置,第二层循环结束交换两个位置。
def select_sort(li):
# i 表示第i趟 无序区的范围 i ~ n-1
for i in range(len(li) - 1):
# 默认最小值是无序区第一个数
min_loc = i
# 循环查找无序区的最小值,并记录位置
for j in range(i + 1, len(li)):
if li[j] < li[min_loc]:
min_loc = j
# 将本趟无序区的第一个位置和无序区最小值交换位置
if min_loc != i:
li[i], li[min_loc] = li[min_loc], li[i]
插入排序动图演示:
插入排序代码:O(n²)
默认第一个值为有序区,第一层for循环控制总趟数,获取无序区第一个值为tem,while循环依次比较tem和有序区的值并依次向后移动一位,最后将tem插入对应位置。
def insert_sort(li):
for i in range(1, len(li)):
tem = li[i]
j = i - 1
while j > -1 and tem < li[j]:
li[j], li[j + 1] = li[j + 1], li[j]
j -= 1
else:li[j+1] =tem
三、列表排序---快速排序、堆排序、归并排序
快排的特点:快、效率高
快排思路:
1.取一个元素p(第一个元素),使元素p归位;
2.列表被p分成两部分,左边都比p小,右边都比p大;
3.递归完成排序。
快排的时间复杂度:O(nlogn)
def partition(li, left, right):
# 默认取第一个数P进行归位
tem = li[left]
while left < right:
# left,right两个指针分别从两边相向查找,右边小于P的值交换到左边
while left < right and li[right] >= tem:
right -= 1
li[left] = li[right]
# 左边大于P的值交换到右边
while left < right and li[left] <= tem:
left += 1
li[right] = li[left]
# 直到left、right两个指针重合,将p值插入完成
li[right] = tem
return right
def quick_sort(li, left, right):
if left < right:
mid = partition(li, left, right)
quick_sort(li, left, mid - 1)
quick_sort(li, mid + 1, right)
四、堆排序前传——树与二叉树简介
树的定义:
树是一种可以递归定义的数据结构,是由n个节点组成的集合:当n=0,那这是一棵空树;当n>0,那存在1个节点作为树的根节点,其他节点可以分为m个集合,每个集合本身又是一棵树。
树的应用:目录,评论等
树的一些概念:
根节点:树的一个组成部分,也叫树根。所有非空的二叉树中,都有且仅有一个根结点。它是同一棵树中除本身外所有结点的祖先,没有父结点。
叶子节点:一棵树当中没有子结点(即度为0)的结点称为叶子结点。
树的深度(高度):树中节点的最大层次。
树的度:一个节点含有的子树的个数称为该节点的度,最大的节点的度称为树的度。
孩子节点/子节点:一个节点含有的子树的根节点称为该节点的子节点。
父节点:若一个节点含有子节点,则这个节点称为其子节点的父节点。
子树:设T是有根树,a是T中的一个顶点,由a以及a的所有后裔(后代)导出的子图称为有向树T的子树。
特殊且常用的树—二叉树
二叉树:度不超过2的树(节点最多有两个叉)
两种特殊二叉树:
满二叉树:一个二叉树,如果每一个层的结点数都达到最大值,则这个二叉树就是满二叉树。
完全二叉树:叶节点只能出现在最下层和次下层,并且最下面一层的结点都集中在该层最左边的若干位置的二叉树。
二叉树的存储方式:
1.链式存储方式
2.顺序存储方式(列表)
父节点和左孩子节点的编号下标关系:父节点为 i ---> 左孩子节点 2i+1
父节点和右孩子节点的编号下标关系:父节点为 i---> 左孩子节点 2i+2
五、堆排序
堆
大根堆:一棵完全二叉树,满足任一节点都比其孩子节点大
小根堆:一棵完全二叉树,满足任一节点都比其孩子节点小
堆的向下调整性质:
当根节点的左右子树都是堆时,可以通过一次向下的调整来将其变换成一个堆。
堆排序过程
1.建立堆
2.得到堆顶元素,为最大元素
3.去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序。
4.堆顶元素为第二大元素。
5.重复步骤3,直到堆变空。
堆排序代码实现:
def sift(li, low, high):
"""
堆的调整函数
:param li:列表
:param low: 根节点
:param high: 堆最后一个元素位置
"""
tmp = li[low]
i = low
j = 2 * i + 1
while j <= high: # 第一个退出条件:j不存在
if j + 1 <= high and li[j + 1] > li[j]: # 如果右孩子存在并且右孩子比左孩子大
j += 1 # j指向右孩子
if li[j] > tmp:
li[i] = li[j]
i = j
j = 2 * i + 1
else: # 第二个退出条件:j位置的值比tmp小
break
li[i] = tmp
def heap_sort(li):
# 1. 建大根堆
n = len(li)
for i in range(n // 2 - 1, -1, -1): # i表示遍历的low
sift(li, i, n - 1) # low:i high:统一写成最后一个位置对正确性没有影响
# 2. 挨个出数:退休 棋子 调整
for i in range(n - 1, 0, -1): # i 表示当前位置堆的high
li[i], li[0] = li[0], li[i] # 根节点也就是最大的数放在最后面
sift(li, 0, i - 1) # 重新调整
六、堆排序—内置模块 heapq
优先队列:
一些元素的集合,POP操作每次执行都会从优先队列中弹出最大(或最小)的元素。
内置模块 heapq就是利用的优先队列来实现的
Python内置模块—heapq
1.heapify(x) === 建堆,将x转成小根堆形式。
2.heappush(heap, item) === 向heap中添加item,heap一直维持着小根堆的秩序。
3.heappop(heap) === 从heap中pop一个元素,heap一直维持着小根堆的秩序。
利用heapq模块实现堆排序
def heap_sort(li):
L = []
for i in range(len(li)):
heapq.heappush(L,li[i])
return [heapq.heappop(L) for i in range(len(L))]
七、堆排序扩展——topK问题
需求:现在有n个数,设计算法找出前k大的数(k
解决方法:
1.排序后切片
2.堆的应用
堆排序实现TopK问题:
解决思路:
1.取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。
2.依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。
3.遍历列表所有元素后,倒序弹出堆顶。
堆排序实现TOPK问题代码:
def sift(li, low, high):
"""
堆的调整函数
:param li:列表
:param low: 根节点
:param high: 堆最后一个元素位置
"""
tmp = li[low]
i = low
j = 2 * i + 1
while j <= high: # 第一个退出条件:j不存在
if j < high and li[j + 1] < li[j]: # 如果右孩子存在并且右孩子比左孩子大
j += 1 # j指向右孩子
if li[j] < tmp:
li[i] = li[j]
i = j
j = 2 * i + 1
else: # 第二个退出条件:j位置的值比tmp小
break
li[i] = tmp
def topk(li, k):
# 取列表前k个元素建立一个小根堆。堆顶就是目前第k大的数。
heap = li[0:k]
for i in range(k // 2 - 1, -1, -1):
sift(heap, i, k - 1)
# 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;
# 如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。
print("==", heap)
for i in range(k, len(li)):
if li[i] > heap[0]:
heap[0] = li[i]
print("前==", heap)
sift(heap, 0, k - 1)
print("后==", heap)
# 遍历列表所有元素后,倒序弹出堆顶
for i in range(k - 1, -1, -1):
heap[0], heap[i] = heap[i], heap[0]
sift(heap, 0, i - 1)
return heap
八、归并排序
假设现在的列表分两段有序,如何将其合成为一个有序列表???
这种操作称为一次归并。
一次归并代码实现:
def merge(li, low, mid, high):
# 左半边有序列表第一位
i = low
# 右半边有序列表第一位
j = mid + 1
li_tmp = []
# 如果两边都有数
while i <= mid and j <= high:
# 左边小于右边
if li[i] < li[j]:
li_tmp.append(li[i])
i += 1
else:
li_tmp.append(li[j])
j += 1
# 判断左边列表是否有剩余元素
while i <= mid:
li_tmp.append(li[i])
i += 1
# 判断右边列表是否有剩余元素
while j <= high:
li_tmp.append(li[j])
j += 1
# 将新生成列表赋值回原来列表
for i in range(len(li_tmp)):
li[low + i] = li_tmp[i]
一次归并练习:
有两个有序的列表,通过代码合并成一个有序列表:代码如下:
def marge(li1, li2):
i = 0
j = 0
ret_list = []
while i < len(li1) and j < len(li2):
if li1[i] < li2[j]:
ret_list.append(li1[i])
i += 1
else:
ret_list.append(li2[j])
j += 1
while i < len(li1):
ret_list.append(li1[i])
i += 1
while j < len(li2):
ret_list.append(li2[j])
j += 1
return ret_list
归并排序步骤:
分解:将列表越分越小,直至分成一个元素。
终止条件:一个元素是有序的。
合并:将两个有序列表归并,列表越来越大。
归并排序代码实现:时间复杂度:O(nlogn)
def merge(li, low, mid, high):
# 左半边有序列表第一位
i = low
# 右半边有序列表第一位
j = mid + 1
li_tmp = []
# 如果两边都有数
while i <= mid and j <= high:
# 左边小于右边
if li[i] < li[j]:
li_tmp.append(li[i])
i += 1
else:
li_tmp.append(li[j])
j += 1
# 判断左边列表是否有剩余元素
while i <= mid:
li_tmp.append(li[i])
i += 1
# 判断右边列表是否有剩余元素
while j <= high:
li_tmp.append(li[j])
j += 1
# 将新生成列表赋值回原来列表
for i in range(len(li_tmp)):
li[low + i] = li_tmp[i]
def _merge_sort(li, low, high):
if low < high: # 至少两个元素
mid = (low + high) // 2
_merge_sort(li, low, mid)
_merge_sort(li, mid + 1, high)
merge(li, low, mid, high)
补充:在列表元素10万左右进行排序时,使用系统的sort()排序所需时间在0.03s-0.05s;使用python实现的快排所需时间在0.4-0.5s之间;堆排序在0.65s-0.7s之间;归并排序在0.65s-0.7s之间(时间长短因为电脑配置不同有差异)。这是因为系统的sort排序的底层是 C语言实现的,从而可以知道python语言的执行效率相对C语言的执行效率要低很多。
快排、堆排、归并三种排序算法的时间复杂度都是O(nlogn)
一般情况下,就运行时间而言:快速排序 < 归并排序 < 堆排序
三种排序算法的缺点:
快速排序:极端情况下排序效率低
归并排序:需要额外的内存开销
堆排序:在快的排序算法中相对较慢