人工智能
- python,大数据,机器学习,深度学习,计算机视觉
-
三、python算法篇(五)排序和查找
python,大数据,机器学习,深度学习,计算机视觉
三、python算法篇(五)排序和查找
前言 ---- 排序算法的稳定性
一个例子说明白!
比如,对这几个元组(4, 1) (3, 1) (3, 7) (5, 6)
,按照第一个元素的大小对他们进行排序,由于(3, 1)和(3, 7)的第一个元素是相等的,所以有下面两种排序结果:
(1)(3, 1) (3, 7) (4, 1) (5, 6) (维持原有次序)----- 稳定
(2)(3, 7) (3, 1) (4, 1) (5, 6) (原有次序被改变) ----- 不稳定
我们称第(1)种维持原有次序的排序算法是稳定的,而第(2)种改变了原有次序的排序算法是不稳定的。
同理:若原次序为 (4, 1) (3, 7) (3, 1) (5, 6)
,对其排序,则排序结果:
(1)(3, 7) (3, 1) (4, 1) (5, 6) (维持原有次序)----- 稳定
(2)(3, 1) (3, 7) (4, 1) (5, 6) (原有次序被改变) ----- 不稳定
1. 冒泡排序
自己画了个图,便于彻底理解!不必死记用背
(1)基本写法。下面这种写法太简单!不多说
09_bubble_sort.py代码:
def bubble_sort(alist):
"""冒泡排序"""
n = len(alist)
"""j控制循环次数。共n-1次"""
for j in range(1, n): # 表1 ~ n-1,总共冒泡n-1次。 ||或写 range(0, n-1):即ragne(n-1):但下面要改成for i in range(n-1-j):即for i in range(0, n-1-j):
"""i控制下标/游标。从0开始,第一次一直移动到第n-1个元素(倒数第二个元素)即下标n-1-1即可,因为自然会取到第n个元素(最后一个元素)下标n-1"""
for i in range(n-j): # 即range(0, n-j):表0 ~ n-j-1
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i] #python专有的交换语法
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
bubble_sort(li)
print(li)
思考:
想想若本来就是有序的序列[1, 2, 3, 4, 5, 6, 7],还要再都依次比较吗?
再想想若除了前两个元素其他都是有序的序列[2, 1, 3, 4, 5, 6, 7],还要这么写吗?
(2)改进的冒泡排序。----- 可处理一开始就是有序 或者 排序过程中的某次就已经是有序的
写法一:
def bubble_sort(alist):
"""冒泡排序"""
n = len(alist)
"""j控制循环次数。共n-1次"""
for j in range(n-1): #每次外层for循环开始,标记号count都重置为0
count = 0
"""i控制下标/游标。从0开始,第一次一直游标到倒数第二个元素(第n-1个元素)即下标n-1-1即可,因为自然会取到最后一个元素(第n个元素)下标n-1"""
for i in range(0, n-1-j): #若内层for循环有过交换,则count+1
# 即:for i in range(n-1-j):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i] #python专有的交换语法
count += 1 # python不能写count++
if 0 == count: #若没有过交换,即顺序是排好了的,则不需再遍历了。
return
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
bubble_sort(li)
print(li)
写法二:
def bubble_sort(alist):
"""冒泡排序"""
n = len(alist)
"""j控制循环次数。共n-1次"""
for j in range(n-1): #每次外层for循环开始,标记号count都重置为0
swap = False
"""i控制下标/游标。从0开始,第一次一直游标到倒数第二个元素(第n-1个元素)即下标n-1-1即可,因为自然会取到最后一个元素(第n个元素)下标n-1"""
for i in range(0, n-1-j): #若内层for循环有过交换,则count+1
# 即:for i in range(n-1-j):
if alist[i] > alist[i+1]:
alist[i], alist[i+1] = alist[i+1], alist[i] #python专有的交换语法
swap = True # 交换过
if swap == False: #若没有过交换,即顺序是排好了的,则不需再遍历了。
return
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
bubble_sort(li)
print(li)
最优时间复杂度:O(n) ------ 遍历一次发现没有任何可以交换的元素,排序结束
最坏时间复杂度:O(n2)
算法稳定性:稳定 ----- 前面讲的能维持原有次序
2. 选择排序
根本思路:不断地找出相对最小的数,放到开头。
实现过程:
如 li = [54, 23, 32, 324, 34, 324, 3]
首先,默认第一个元素li[0] = 54是最小的数,记录下标min_index = 0(实际代码记录下标,列表/数组就是玩下标的)
然后,其他的元素都和最小数比较,谁更小就重定谁是最小数:
比到第23时,最小数成了23,记录其下标min_index = 1
再往后走,324…和最小数23比较…,一直到3出现,最小数成了3,记录其下标min_index = 6
所以将最小数放到最左边开头,即交换 li[6] 和 li[0]
此为一轮。
接着,不管li[0],再将li[1]看出最小的数,重复上面操作,进行几轮操作。如下:
一轮:li = [3, 23, 32, 324, 34, 324, 54]
二轮:li = [3, 23, 32, 324, 34, 324, 54]
三轮:li = [3, 23, 32 324, 34, 324, 54]
四轮:li = [3, 23, 32, 34 324, 324, 54]
五轮:li = [3, 23, 32, 34, 54 324, 324]
六轮:li = [3, 23, 32, 34, 324, 324]
代码思路:
(1)初步代码设计:
def select_sort(alist):
n = len(alist)
min_index = 0
for i in range(1,n):# j: 1 ~ n-1
if alist[min_index]>alist[i]:
min_index = i
alist[min_index], alist[i] = alist[i], alist[min_index]
其实也可以按下面这样设计,但不如上面的效率高,因为alist[min_index], alist[i] = alist[i], alist[min_index]交换是执行两步(其他语言temp那种交换就是三步),前面已说过,列表(如同其他数组)就是玩下标的。
参考:
def select_sort(alist):
n = len(alist)
min_index = 0
for i in range(1,n):# j: 1 ~ n-1
if alist[min_index]>alist[i]:
alist[min_index], alist[i] = alist[i], alist[min_index]
(2)进一步地:
def select_sort(alist):
n = len(alist)
for j in range(n-1):
min_index = j
for i in range(j+1, n):
if alist[min_index] > alist[i]:
min_index = i
alist[j], alist[min_index] = alist[min_index], alist[j]
完整代码如下:
10_select_sort.py代码:
def select_sort(alist):
"""选择排序"""
n = len(alist)
for j in range(n-1): # 即range(0, n-1) 故j: 0 ~ n-2
min_index = j
for i in range(j+1, n):
if alist[min_index] > alist[i]:
min_index = i
alist[j], alist[min_index] = alist[min_index], alist[j]
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
select_sort(li)
print(li)
最优时间复杂度:O(n2) ------ 即使是排序好的有序序列也是O(n2)
最坏时间复杂度:O(n2)
算法稳定性:不稳定 ----- 有重复值时,考虑升序每次选择最大的情况
3. 插入排序
插入排序和选择排序一样都是分成左右两部分:左边(下标较小的)是排好序的,右边(下标较大的))是无序的。
不同的是:
上面选择排序是从右边选出最小值放到左边。
而插入排序是从右边随便拿一个,实际操作也就是一个个依次拿,和左边的去比较,排序插入到左边。即插入排序名字由此而来!
(1)基本写法。
代码思路:
(1)初步代码设计:
def insert_sort(alist):
j = 1 # i = [1, 2, 3, ..., n-1]
# 执行从右边的无序序列中取出第一个元素,即i位置的元素,然后将其插入到前面的正确位置中
# 从第i个元素开始向前比较,如果小于前一个元素,交换
for i in range(j, 0, -1): # j:从i ~ 1,递减。如range(5, 0, -1)是[5,4,3,2,1],最后位是1不是0,最后位 = 0-步长 = 0-(-1) = 1
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
(2)进一步地:
def insert_sort(alist):
n = len(alist)
for j in range(1, n):# j = [1, 2, 3, ..., n-1]
# 执行从右边的无序序列中取出第一个元素,即i位置的元素,然后将其插入到前面的正确位置中
# 从第i个元素开始向前比较,如果小于前一个元素,交换
for i in range(j, 0, -1): # j:从i ~ 1,递减。如range(5, 0, -1)是[5,4,3,2,1],最后位是1不是0,最后位 = 0-步长 = 0-(-1) = 1
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
完整代码如下:
11_insert_sort.py代码:
def insert_sort(alist):
""""插入排序"""
n = len(alist)
for j in range(1, n):# j = [1, 2, 3, ..., n-1]
# 执行从右边的无序序列中取出第一个元素,即i位置的元素,然后将其插入到前面的正确位置中
for i in range(j, 0, -1): # j:从i ~ 1,递减。
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
insert_sort(li)
print(li)
(2)改进的插入排序。----- 可处理一开始就是有序 或者 排序过程中的某次就已经是有序的
代码思路:
(1)初步代码设计:
def insert_sort(alist):
"""插入排序"""
i = 1 # i = [1, 2, 3, ..., n-1]
while i > 0:
if alist[i] < alist[i-1]:
alist[i],alist[i-1] = alist[i-1], alist[i]
i -= 1
else:
break
(2)进一步地:
def insert_sort(alist):
n = len(alist)
for j in range(1, n): # j = [1, 2, 3, ..., n-1]
i = j # i 代表内存循环起始值
# 执行从右边的无序序列中取出第一个元素,即i位置的元素,然后将其插入到前面的正确位置中
while i > 0:
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1
else:#如果a[i]比左边a[i-1]大,则退出本次while循环,想想[1,2,3,4,5]的情况。
break
完整代码如下:
11_insert_sort.py代码:
def insert_sort(alist):
"""插入排序"""
n = len(alist)
for j in range(1, n): # j = [1, 2, 3, ..., n-1]
i = j # i 代表内存循环起始值
# 执行从右边的无序序列中取出第一个元素,即i位置的元素,然后将其插入到前面的正确位置中
while i > 0:
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
i -= 1
else:
break
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
insert_sort(li)
print(li)
最优时间复杂度:O(n)----- 升序排列,序列已经处于升序状态
最坏时间复杂度:O(n2)
稳定性:稳定
4. 希尔排序
希尔排序和插入排序差不多,唯一的区别是希尔排序有步长。
首先步长gap设计为4,
第一组:下标为0,4, 8的拿出来分成一组(步长gap=4,所以0,4,8)
第二组:下标为1,5, 的拿出来分成一组
第三组:下标为2,6, 的拿出来分成一组
…
然后每组组内用插入排序算法比较。
接下来将步长gap设计为2,再重复步骤,同上(我们暂时用折半的方法取步长值)
最后,直到gap=1时,再用插入排序法,结束。
看似麻烦,实则若gap设计好(实际中怎么给gap最优设计需要通过数学计算得到),效率很高。
代码思路:
(1)首先考虑核心代码,开始步长gap=1时,
#第一步 gap = 1
if alist[i] < alist[i-1]:
alist[i], alist[i-1] = alist[i-1], alist[i]
else:
break
(2)进一步地,gap = 2,4,8等。
完整代码如下:
12_shell_sort.py代码:
def shell_sort(alist):
"""希尔排序"""
n = len(alist)
# gap具体怎么取会影响效率,最优取法要通过数学运算来算出,这里不研究,下面暂时用折半的方法(如第一次取4,下一次取2,再下一次取1)取gap值
gap = n//2 # 除法运算。python3 n/2得到小数,n//2得到整数。而老版本的python2可以写n/2得到整数
# gap变化到0之前,插入算法执行的次数
while gap > 0:
for j in range(gap, n): # 希尔排序,与普通的插入算法的区别就是gap步长
# j = [gap, gap+1, gap+2, gap+3,..., n-1]
i = j
while i > 0:
if alist[i] < alist[i-gap]:
alist[i], alist[i-gap] = alist[i-gap], alist[i]
i -= gap
else:
break
# 缩短gap步长
gap //= 2
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
shell_sort(li)
print(li)
注:可以对比一下插入排序的代码,对比理解和记忆。
最优时间复杂度:O(n1.3) ;根据步长(上面的gap)序列的不同而不同
最坏时间复杂度:O(n2) ------- gap直接取1的时候,就是插入排序
稳定性:不稳定 ------ 假设排序序列中有两个相同又相邻的数77和77,因为分成多组,第一个77被分到的那组有可能在组里面是最大值或相对较大的排序到后面,而第二个77分到的组如果其在组里是最小值或相对比较小的被排序到了前面,这样一来最终结局就是原来的第一个77跑到了第二个77后面,所以不稳定
5. 快速排序(重点,必须掌握)
快排一句话:序列中任意取一个元素作为目标(中间值mid_value),其余元素比他小的放到他左面,比他大的放到右面。然后左右两面重复上面操作,再取一个中间值…
【分析】:
第一步:
def quick_sort(alist, first, last): # ([54, 23, 32, 324, 34, 324, 3], 0, 6)
mid_value = alist[first]
left = first
right = last
# 代码核心思想:
# 比中间值(mid_value)小的放在左边(left),比中间值大的放在右边(right)。
while left < right:
alist[left] = alist[right]
alist[right] = alist[left]
# 从上面循环退出时,left == right
alist[left] = mid_value
# 递归:
# 对left左边的列表执行快速排序
quick_sort(alist, first, left-1)
# 对left右边的列表进行快速排序
quick_sort(alist, left+1, last)
第二步:
def quick_sort(alist, first, last): # ([54, 23, 32, 324, 34, 324, 3], 0, 6)
"""快速排序"""
if first >= last:
return
mid_value = alist[first] # 54
left = first # 0
right = last # 6
# 比中间值(mid_value)小的放在左边(left),比中间值大的放在右边(right)。
while left < right: # 0 < 6
# 只要右边的比中间值大,right就一直左移:因为要从右向左找比中间值小的放到左边left里。
while left < right and alist[right] >= mid_value:
right -= 1
alist[left] = alist[right]
# 只要左边的比中间值小,left就一直右移:因为要从左向右找比中间值大的放到右边right里。
while left < right and alist[left] < mid_value:
left += 1
alist[right] = alist[left]
# 从上面循环退出时,left == right
alist[left] = mid_value
# 对left左边的列表执行快速排序
quick_sort(alist, first, left-1) # 递归思想
# 对left右边的列表进行快速排序
quick_sort(alist, left+1, last)
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
quick_sort(li, 0, len(li)-1)
print(li)
完整代码如下,也可将left写成low低位、right写成high高位:
13_quick_sort.py代码:
def quick_sort(alist, first, last):
"""快速排序"""
if first >= last:
return
mid_value = alist[first]
low = first
high = last
while low < high:
# high左移
while low < high and alist[high] >= mid_value:
high -= 1
alist[low] = alist[high]
# low右移
while low < high and alist[low] < mid_value:
low += 1
alist[high] = alist[low]
# 从循环退出时,low == high
alist[low] = mid_value
# 对low左边的列表执行快速排序
quick_sort(alist, first, low-1)
# 对low右边的列表进行快速排序
quick_sort(alist, low+1, last)
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
quick_sort(li, 0, len(li)-1)
print(li)
最优时间复杂度:O(nlogn) ------ 第一次分成左右两组,左右总共执行n次;第二次再分成22=4组,共执行n次;第三次分成22*2=8组,共执行n次…,直到不可再分割时即每组只有一个元素时。可见总共分割了log2n次(简记作logn),每次执行了n次,所以最优时间复杂度O(nlogn)
最坏时间复杂度:O(n2)
稳定性:不稳定
6. 归并排序
归并排序是采用分治法的一个非常典型的应用。归并排序的思想就是先递归分解数组,再合并数组。
将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁最小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
代码思路:
(1)初步代码设计:递归实现拆分!
def merge_sort(alist):
n = len(alist)
mid = n//2
left_li = merge_sort(alist[:mid])
right_li = merge_sort(alist[mid:])
(2)写递归一定注意写终止条件,避免死循环。这里不能无限拆分,拆到列表长度 为1时就不拆了!
def merge_sort(alist):
n = len(alist)
if n <= 1:
return
mid = n//2
min = merge_sort(alist[:mid])
完整代码如下:
14_merge_sort.py代码:
def merge_sort(alist):
"""归并排序"""
n = len(alist)
if n <= 1:
return alist
mid = n//2
# 左部分列表: 采用归并排序后形成的有序的新的列表
left_li = merge_sort(alist[:mid])# alist[:mid]表截取从开头到mid即下标0~mid的部分列表
# 右部分: 采用归并排序后形成的有序的新的列表
right_li = merge_sort(alist[mid:])# alist[mid:]表截取从mid到结尾即下标mid~len-1的部分列表
# 将左右两个有序的子序列合并为一个新的整体
left_pointer, right_pointer = 0, 0 # 指针,记录下标
result = [] # 存放排序后的列表
while left_pointer < len(left_li) and right_pointer < len(right_li):
if left_li[left_pointer] <= right_li[right_pointer]:#等号保证了此算法(归并排序算法)的稳定-----见时间复杂度那里总结。若不加等号则变不稳定
result.append(left_li[left_pointer])
left_pointer += 1
else:
result.append(right_li[right_pointer])
right_pointer += 1
result += left_li[left_pointer:]
result += right_li[right_pointer:]
return result
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(li)
#重点:
merge_sort(li)#归并排序不改变原列表顺序,因为此法是新建一个result存放新的列表,从原列表alist往result里面放值
print(li)
print(merge_sort(li))
最优时间复杂度:O(nlogn) ------ 看上面开始的图,第一步(合1)需要执行n次,第二步(合2)需要执行n次
最坏时间复杂度:O(nlogn)
稳定性:稳定
只有归并排序算法需要开辟一块新的空间,虽然时间上它是好的效率高的,但是空间上它是大的。
排序算法总结对比
总结:
需要掌握3、4种排序算法不要求全部掌握。
但是!快排必须掌握,因为快排的时间复杂度最低效率最高(看最坏情况,见上表)应用是最广泛的!
虽然归并排序时间复杂度和快排一样好,但归并排序需要另开辟一额外的空间存放新列表占用空间大,所以快排是目前最优的排序算法!
虽然快排不稳定,但在实际应用中我们一般不需要保序(值相等的时候保持原次序),如果需要保序那就不用快排了。
7. 二分查找(折半查找)
联想实际查字典怎么查,让你查你的姓,你实际试试。比如“令狐”(Linghu),肯定是字典拿来中间随便翻开一页,如果你翻到的拼音在你的姓拼音之前比如你翻到了拼音H位置(你要找的令狐拼音开头L在H后),那么就再往后面随便一翻;如果你翻到的拼音在你的姓拼音之后比如你翻到了拼音O位置(你要找的令狐拼音开头L在O前),那么就再往前面随便一翻…如此下去,直到找到为止。
在实际的查找案例中,若找不到,则说明没有我们可以返回False了。
注意:
使用二分查找(折半查找)的前提:被查的列表是有序列表,即升序或降序,如1, 3, 4, 7, 43,344。或者3223, 233, 23, 29, 6, 2这样的
法1(递归法):
15_binary_search.py代码:
def binary_search(alist, item):
"""二分查找(递归法)"""
n = len(alist)
if n > 0:
mid = n//2# 一定记住双斜线除号得到整数,单斜线除号得到小数
if alist[mid] == item:
return True
elif item < alist[mid]:
return binary_search(alist[:mid], item)#这里一定记得写return
else:
return binary_search(alist[mid+1:], item)#这里一定记得写return
return False
#test
if __name__ == "__main__":
li = [54, 23, 32, 324, 34, 324, 3]
print(binary_search(li, 23))#23在li列表中,结果True
print(binary_search(li, 100))#100不在li列表中,结果False
"""
运行结果:
True
False
"""
法2(first、last法):
def binary_search_2(alist, item):
"""二分查找(非递归法)"""
n = len(alist)
first = 0#起始。这个叫first、last法,或者叫start、end法
last = n-1#末尾
while first <= last:
mid = (first+last)//2
if alist[mid] == item:
return True
elif item < alist[mid]:
last = mid-1
else:
first = mid+1
return False
最优时间复杂度:O(1) ------ 如li = [54, 23, 32, 324, 34, 324, 3],查找54。因为54在首元素,则执行一次即找到。
最坏时间复杂度:O(logn) ------ 同上面讲的算法,二分查找即折半查找,每次分成两部分,执行一次,共分了logn次(2的多少次幂),即1*logn = logn
8. 堆排序
def buildMaxHeap(arr):
import math
for i in range(math.floor(len(arr)/2),-1,-1):
heapify(arr,i)
def heapify(arr, i):
left = 2*i+1
right = 2*i+2
largest = i
if left < arrLen and arr[left] > arr[largest]:
largest = left
if right < arrLen and arr[right] > arr[largest]:
largest = right
if largest != i:
swap(arr, i, largest)
heapify(arr, largest)
def swap(arr, i, j):
arr[i], arr[j] = arr[j], arr[i]
def heapSort(arr):
global arrLen
arrLen = len(arr)
buildMaxHeap(arr)
for i in range(len(arr)-1,0,-1):
swap(arr,0,i)
arrLen -=1
heapify(arr, 0)
return arr
9. 计数排序
def countingSort(arr, maxValue):
bucketLen = maxValue+1
bucket = [0]*bucketLen
sortedIndex =0
arrLen = len(arr)
for i in range(arrLen):
if not bucket[arr[i]]:
bucket[arr[i]]=0
bucket[arr[i]]+=1
for j in range(bucketLen):
while bucket[j]>0:
arr[sortedIndex] = j
sortedIndex+=1
bucket[j]-=1
return arr
10. 基数排序
def radix(arr):
digit = 0
max_digit = 1
max_value = max(arr)
#找出列表中最大的位数
while 10**max_digit < max_value:
max_digit = max_digit + 1
while digit < max_digit:
temp = [[] for i in range(10)]
for i in arr:
#求出每一个元素的个、十、百位的值
t = int((i/10**digit)%10)
temp[t].append(i)
coll = []
for bucket in temp:
for i in bucket:
coll.append(i)
arr = coll
digit = digit + 1
return arr