排序
排序算法的稳定性
稳定,即对于相等的元素,排序算法不会改变其原有的次序。
如果被排序的数是整数,稳定不稳定并不重要。但是如果是一个序列、集合或者映射,要按照他们的某一个元素进行排序,那么对于相等元素保持原有次序或许非常重要。
冒泡排序
最简单,最符合常规思路。
思路
每一轮都把最大的那个沉到最下面。
由于剩下的最后一轮只有一个元素,故而只需进行n-1轮,每轮比较n-1次
当然你要进行n轮也行。但是每轮最多是n-1次,防止j+1越界。
二重循环:
外层记录轮数
内层进行一轮下沉操作
实现
# 冒泡
# 最坏O(n^2)
# 最优O(n)
# 稳定
def bubble_sort(lst):
for i in range(len(lst) - 1):
for j in range(len(lst) - 1 - i):
if lst[j] > lst[j + 1]:
lst[j], lst[j + 1] = lst[j + 1], lst[j]
# 对冒泡进行最优化:
# 对有序序列不做处理:
def op_bubble_sort(lst):
for i in range(len(lst) - 1):
count = 0
for j in range(len(lst) - 1 - i):
if lst[j] > lst[j + 1]:
lst[j], lst[j + 1] = lst[j + 1], lst[j]
count += 1
if count == 0:
break
#return
选择排序
与冒泡排序思路几乎相同
又不稳定,效率也不高,用得很少。
思路
二重循环:
外层:记录轮数,并将找到的最大值沉底(最小值上浮)
内层:找到最大的(最小)的
下沉:
进行N-1轮
每轮(第i轮)挑选出序列中未排序部分中最大的那个元素与下标为N-i的元素交换位置(无序序列中最后一个)
在列表尾部形成有序数列
上浮:
进行N-1轮
每轮(第i轮)挑选出最小的那个元素,与下标为i的元素(无序序列中的首元素)交换位置。
在列表头部形成有序数列
实现
# 选择排序
# 最坏O(n^2)
# 最优O(n^2):并不能通过一次循环判断出序列是否是有序的。
# 不稳定
def selection_sort(lst):
for i in range(len(lst) - 1):
max = lst[0]
idx = 0
# 如果从0开始会多比较一次,其实也没什么
for j in range(1, len(lst) - i):
if lst[j] > max:
max = lst[j]
idx = j
# j是子序列最后一个元素下标
if lst[idx] != lst[j]:
lst[idx], lst[j] = lst[j], lst[idx]
def selection_sort2(lst):
for i in range(len(lst) - 1):
min = lst[i]
idx = i
for j in range(i + 1, len(lst)):
if lst[j] < min:
min = lst[j]
idx = j
# i是子序列首元素下标
if lst[idx] != lst[i]:
lst[i], lst[idx] = lst[idx], lst[i]
# 另一种更为精炼的写法
# 上浮:
def selection_sort3(lst):
for i in range(len(lst) - 1):
min_index = i
for j in range(i + 1, len(lst)):
if lst[j] < lst[min_index]:
min_index = j
if min_index != i:
lst[i], lst[min_index] = lst[min_index], lst[i]
# 下沉:
def selection_sort4(lst):
for i in range(len(lst) - 1):
max_index = 0
for j in range(1, len(lst) - i):
if lst[j] > lst[max_index]:
max_index = j
if max_index != j:
lst[j], lst[max_index] = lst[max_index], lst[j]
插入排序
在列表头部逐渐形成有序数列。
思路
将首元素作为第一个有序数列,将后面的1到n-1个数插到前面。
进行N-1轮
从第二个元素(下标为1)开始,每轮将元素插入到有序数列的应有位置上。
二重循环
外层记录轮数(需要插入的数字个数)
内层进行插入操作
插入方法:
与它前面的数比较,如果比自己大,就一直交换下去(倒着来的)
需要注意的是每次交换过后,插入数的下标会改变
实现
# 插入排序
# 最坏O(n^2)
# 最优O(n)
# 稳定
def insert_sort0(lst):
# lst[i]是每轮插入的数,i同时也是有序数列长度
for i in range(1, len(lst)):
# 倒着与有序数列中的数进行比较
# 注意每次交换过后,插入的数在序列中的下标会改变
swap_index = i
for j in range(i - 1, -1, -1):
if lst[swap_index] < lst[j]:
lst[swap_index], lst[j] = lst[j], lst[swap_index]
swap_index = j
# 阿西,刚才那个实现的真是啰嗦,我每次都用j不就可以了吗!
# 就不用再设置swap变量了,因为对于每次lst[j]不就是要插入的元素吗!!!
def insert_sort(lst):
for i in range(1, len(lst)):
for j in range(i, 0, -1):
if lst[j] < lst[j - 1]:
lst[j], lst[j - 1] = lst[j - 1], lst[j]
#最优化处理:
def op_insert_sort(lst):
for i in range(1, len(lst)):
for j in range(i, 0, -1):
if lst[j] < lst[j - 1]:
lst[j], lst[j - 1] = lst[j - 1], lst[j]
#因为前面是有序数列,如果大于等于最后一个,那肯定大于所有,就不用进行这一轮了。
else:
break
希尔排序
是插入排序的改进版。又称“缩小增量排序”或“递减增量排序”。由Dr.Shell在1959年提出。
基于插入排序在“当序列基本有序时效率接近线性”的特点,以及插入排序每次只能移动一个元素的缺陷对插入排序进行改进。
思路
每个gap都将原序列按照gap步长打断为gap个子序列。
每个序列的最大长度是len//gap+1,一般长度为:len//gap。
lst[0],lst[1],……,lst[gap-1] 分别是每个序列的首元素。
将每次gap下的所有子序列分别进行直接插入排序。
当gap很大时,每个子序列长度很小,可以认为序列基本有序。所以很高效。
每取一个gap排序一次,整个序列都会更接近“基本有序”。
gap折半递减。
就这样高效地轮巡下去……
最终得到一个有序数列。
实现
# 希尔排序。改进版插入排序。
# 非吾等凡人能想出来的方法。
# 最优:取决于步长,可以达到O(n^1.3)
# 最坏:O(n^2)
# 不稳定:相等元素可能位于不同子序列中。
def shell_sort(lst):
n = len(lst)
gap = n // 2
# while gap >= 1:
while gap > 0:
# 进行插入排序,外层走过了所有子序列的轮数。
# i = [gap,gap+1,gap+2,gap+3,……,n-1]
# 当i = k*gap(k=0,1,2……) 时是在对子序列0进行操作;
# 当i = 1 + k*gap 时,是在对子序列1进行操作;
# 依次类推
for i in range(gap, n):
for j in range(i, 0, -gap):
if lst[j] < lst[j - gap]:
lst[j], lst[j - gap] = lst[j - gap], lst[j]
else:
break
gap = gap // 2
# 答案解法:
def shell_sort2(lst):
n = len(lst)
gap = n // 2
while gap > 0:
for i in range(gap, n):
j = i
# 因为j按gap递减,所以j > 0 (j >= gap )即可。
while j > 0 and lst[j] < lst[j - gap]:
lst[j], lst[j - gap] = lst[j - gap], lst[j]
j -= gap
#没有做最优化处理。
gap = gap // 2
快速排序
“分治法”排序。
“最快排序。”
对大数据集效率很高,但是递归的最后几层,效率不高。
思路
选出一个基准。
每次递归都把比基准小的放左边,比基准大的放右边
当分区为空或1时,递归结束。
平均时间复杂度为O(nlogn),比最坏时间复杂度O(n2)有代表性。
实现
#快速排序
# 最优:O(nlogn)
# 平均:O(nlogn)
# 最坏:O(n^2)
# 不稳定:比如当基准取中间值,基准左右都有等值,快排会把所有等值都放在基准的一侧。
#把小于放左侧,大于等于放右侧(若以首元素作为基准,这样稳定)。
def sub_quick_sort(lst,start,end):
#使用if len(lst) < 2:会出错。
#空或长度为1,start==end不能包含所有情况。
if start >= end:
return
low = start
high = end
#将首元素缓存起来,low的位置就空出来了。
mid_value = lst[start]
while low != high:
#找到第一个需要换位置的
#因为循环中high始终在移动,所以还需要low!=high保证不越界。
while low != high and lst[high] >= mid_value:
high -= 1
#交换后high位置空出
lst[low] = lst[high]
#找到需要换位置的low
while low != high and lst[low] < mid_value:
low += 1
#交换后low位置又空出
lst[high] = lst[low]
#mid_index = high也可
mid_index = low
lst[mid_index] = mid_value
sub_quick_sort(lst,start,mid_index-1)
sub_quick_sort(lst,mid_index+1,end)
#这是一个思想偏向C风格的排序,直接在列表上进行原位修改来排序
#选取首元素作为基准(pivot)
def quick_sort(lst):
start = 0
end = len(lst)-1
sub_quick_sort(lst,start,end)
#一个python风格的快速排序:
#没有原位修改lst,而是返回了一个排好序的列表。
#会额外使用更多空间。
#为便于理解选取中间元素作为基准
def quick_sort_pythonic(lst):
if len(lst) < 2:
return lst
mid_index = len(lst)//2
mid_value = lst[mid_index]
left,right = [],[]
lst.pop(mid_index)
for x in lst:
if x < mid_value:
left.append(x)
elif x >= mid_value:
right.append(x)
else:
pass
return quick_sort_pythonic(left) + [mid_value] + quick_sort_pythonic(right)
归并排序
也是分治思想的排序。比快排稍慢,但是稳定。
思路
先递归不断二分,直到子序列长度为1,再逐级合并。
递归深度为logn,合并算法开销为n。
合并算法很容易:
两个指针指向两个待合并序列,每次把小的插入temp,每插入一次,相应指针就后移一位。
注意还要考虑序列长度不等的情况。
实现
# 归并排序
# 最优:O(nlogn)
# 平均:O(nlogn)
# 最坏:O(nlogn)
# 稳定
# 空间:O(n)
def merge_sort(lst):
left = 0
right = len(lst)-1
sub_merge_sort(lst,left,right)
def sub_merge_sort(lst, left, right):
#其实对于算法本身而言if left==right:就够了
# > 是为了排除空链表。
if left >= right:
return
mid = (left+right)//2
sub_merge_sort(lst,left,mid)
sub_merge_sort(lst,mid+1,right)
merge(lst,left,mid,right)
#并不需要真的都分成一个一个小序列,只需要把起始和终点的下标记住即可。
def merge(lst, left, mid, right):
"""
对列表[left:mid+1],[mid+1:right+1]进行合并。
下标分别是left~mid和mid+1~right。
"""
p_left = left
p_right = mid + 1
temp = []
# 循环结束后p_left要么大于mid,要么指向下一个需要被插入的元素
while p_left <= mid and p_right <= right:
if lst[p_right] < lst[p_left]:
temp.append(lst[p_right])
p_right += 1
else:
temp.append(lst[p_left])
p_left += 1
#如果left或right序列中有没走完的情况:
while p_left <=mid:
temp.append(lst[p_left])
p_left += 1
while p_right <= right:
temp.append(lst[p_right])
p_right += 1
#列表的片段赋值。区间左闭右开。
lst[left:right+1] = temp
桶排序
思路
根据桶本身的有序性,将待排序数字依次放入桶中,再挨个将桶中的数倒出来。
假设有n个数字,有m个桶,如果数字是平均分布的,则每个桶里面平均有n/m个数字。
比如有一万个落在在区间[0,1000]内的待排数字,若采用100个桶(大小在[0,10]的放第一个桶,……),那么每个桶里就有100个待排数字。
如果对每个桶中的数字采用快速排序,那么整个算法的复杂度是:
O(n + m * n/m*log(n/m)) = O(n + nlogn – nlogm)
桶越多,速度越快。桶会占据额外空间。
实现
#一个简单的桶排序:
#只能对正整数序列进行排序,而且最大值是多少,就需要多少个桶(+1)。
#时间:O(n+m),m是桶的长度
#空间:O(n+m)
def barrel_sort(lst):
barrel = [0]*(max(lst)+1)
for x in lst:
barrel[x] += 1
for idx,x in enumerate(barrel):
for i in range(x):
print(idx,end= ' ')
print()
搜索
二分查找
“折半查找”。
适用于有序的顺序表。
思路
不断二分。最终mid指针指向要找的元素。
与归并排序的分割方式一模一样。
实现
#递归实现:
#若找到返回下标。但是若有多个相等元素,只能返回其中一个的下标,而且不能保证是第一个。
#emmm或许返回布尔型更科学???
#递归别忘了写return!
def dichotomy_search(lst, x):
left = 0
right = len(lst) - 1
return dichotomy(lst,left,right,x)
def dichotomy(lst, left, right, x):
mid = (left + right) // 2
if left > right:
return "Not in!"
if x == lst[mid]:
return mid
elif x < lst[mid]:
return dichotomy(lst, left, mid,x)
else:
return dichotomy(lst, mid + 1, right,x)
#循环实现:
def diseciton_search(lst,x):
left = 0
right = len(lst)-1
mid = (left+right)//2
#注意若把条件写成left<=right,当x取值在序列范围之外时,会陷入死循环。
while x != lst[mid] and left < right:
if x < lst[mid]:
right = mid
mid = (left+right)//2
else:
left = mid+1
mid = (left+right)//2
if x == lst[mid]:
return mid
else:
return 'Not in!'