算法集锦
2. 排序
1. 冒泡排序——稳定
思想:相邻两个数两两比较,可以得到最大值(最小)的位置
时间复杂度:O(n3)
空间复杂度:O(n)
实现:
- 未排序数组a[n]
- K = n
- 数组前k个数据两两比较,交换
- K = k-1
- 若k > 1,回到C操作
优化:
- 若一轮比较下来,无交换情况,则有充足理由认为数组已有序,无需继续操作。可以设置标志变量,若出现交换,改变标志变量。一轮比较下来后,查询标志变量是否变化。若标志变量未变,则结束循环,否则,重置标志,继续循环
- 若数组部分有序,且符合规定,则这部分不会出现交换。由此可以推导出,最后一次发生交换的位置之后,已有序。
PS: a = [3, 2, 13, 12, 16, 17, 18]
第一轮比较后,最后一次交换出现在(12, 16),索引为3,则a[3:6]有序
- 一轮正向比较可以获取最右的值,一轮反向比较可以获取最左值
实现:
class Array:
def __init__(self):
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
def bubble(self):
data = self.UN_SORTED_ARRAY
length = len(self.UN_SORTED_ARRAY)
# 是否存在交换,若无,则排序已完成
# 初步优化
flag = False
# 该轮最后一次发生交换时的索引
# 再次优化
# 优化情况:[2, 1, 3, 4, 5]
# 第一轮交换后,最后一次比较索引为1,
# 第二轮无交换,直接退出循环
right_last_swap_index = length-1
left_last_swap_index = -1
for i in range(0, length-1):
for j in range(0, right_last_swap_index):
if data[j+1] < data[j]:
data[j], data[j+1] = data[j+1], data[j]
flag = True
right_last_swap_index = j
pass
pass
# 第三轮优化,冒泡(大值左移,小值右移)同时进行
# 同时记录左移最后一次交换位置
for j in range(left_last_swap_index, -length):
if data[j] < data[j-1]:
data[j], data[j-1] = data[j-1], data[j]
left_last_swap_index = j
flag = True
pass
if not flag:
# 上一轮循环没有交换
# 排序已完成
return data
pass
return data
pass
# 对冒泡排序三种优化进行整合
def bubble_pretty(self):
data = self.UN_SORTED_ARRAY
flag = False
length = len(data)
left_last_swap_index = length - 1
right_last_swap_index = 0
while right_last_swap_index < left_last_swap_index + 1:
for j in range(0, right_last_swap_index):
if data[j+1] < data[j]:
data[j], data[j+1] = data[j+1], data[j]
flag = True
right_last_swap_index = j
pass
pass
for j in range(-1, left_last_swap_index-length):
if data[j] < data[j-1]:
data[j], data[j-1] = data[j-1], data[j]
left_last_swap_index = j
flag = True
pass
if not flag:
return data
pass
2. 计数排序——不稳定
思想:基于统计
过程(脑子清楚,无法表达逻辑):
未排序序列L(n),S(L(i))为L(i)在L(n)中出现次数
扫描整个S,对于每个S(i)∈S,T(i)=S(i)+S(i-1)
T(--L(i))即为排序后L(i)在L(n)位置
优缺点:
优点:空间换时间,时间复杂度下限为O(n+k),k为序列中整数范围。基于比较时间下限为O(nlog2n),算法简单,易于理解。适用于数据范围小,稠密,时间要求高。
缺点:空间换时间。若数据较为发散,稀疏,则会大大浪费空间内存,降低时间效率
实现:
class Array:
def __init__(self):
self.MAX = 10000
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
# 计数排序
def sort_by_count(self):
c = [0] * self.MAX
for val in self.UN_SORTED_ARRAY:
c[val] = c[val] + 1
for i in range(1, self.MAX):
c[i] += c[i-1]
rank = [0]*len(self.UN_SORTED_ARRAY)
for val in self.UN_SORTED_ARRAY:
c[val] -= 1
rank[c[val]] = val
print(rank)
return rank
3. 选择排序
思想:未排序集合S,一次遍历可以获取S最值
过程:
- 待排序序列S,一次遍历,获取其最值S[i],挂到已排序V末端
- 若S未空,继续1操作
时间复杂度:O(n2)
空间复杂度:只需要一个额外的辅助空间(用于交换)
优点:简单,空间需求小,稳定(无论如何,时间复杂度都是O(n2)
缺点:时间性能低下
实现:
class Array:
def __init__(self):
self.MAX = 10000
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
def random_array(self):
import random
ret = list()
for i in self.UN_SORTED_ARRAY:
ret.append(i)
random.shuffle(ret)
return ret
# 选择排序
def selection_sort(self):
arrays = self.random_array()
print("arrays", arrays)
length = len(arrays)
for i in range(0, length-1):
min_val = arrays[i]
min_index = i
for j in range(i, length):
if min_val > arrays[j]:
min_index = j
min_val = arrays[j]
pass
if not min_index == i:
arrays[i], arrays[min_index] = arrays[min_index], arrays[i]
return arrays
4. 直接插入排序
思想:将一个数插入已排序序列S,只需要从S一端开始进行比较,将其插入首次出现满足排序规则的位置
过程:
- 已排序序列S(初始为空),待排序集合V
- 取出V[i],插入序列S中
- 若V不为空,继续2操作
时间复杂度:O(n2)
空间复杂度:需要一个额外的辅助空间
优点:对于近似排序序列,效率高;思想简单,易于实现
缺点:时间复杂度高
实现:
class Array:
def __init__(self):
self.MAX = 10000
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
def random_array(self):
import random
ret = list()
for i in self.UN_SORTED_ARRAY:
ret.append(i)
random.shuffle(ret)
return ret
# 直接插入排序
def insert_sort(self):
array = self.random_array()
length = len(array)
for i in range(1, length):
val = array[i]
for j in range(1, i+1):
index = -j
if val < array[i+index]:
array[i+index+1] = array[i+index]
array[i+index] = val
else:
break
pass
return array
5. 希尔排序
基础:插入排序(对插入排序的优化),基于插入排序对近似排序序列具有高效率的特点
思想:利用插入排序,将未排序序列V化为近似排序序列(分组),最后使用直接插入排序进行排序
时间复杂度:与所取间隔有关,平均优于O(n2)
空间复杂度:1个额外辅助空间
优点:时间效率由于直接插入排序,空间需求低,易于实现
缺点:速度上比不过快速排序,但是时间效率比快排稳定(最坏情况,平均情况相差不大)
过程:
- 将数组分为间隔为gap的若干分组
- 使用直接插入排序对各个分组进行排序
- Gap-- (相邻间隔为1)
- 若gap大于0,继续1操作
实现:
class Array:
def __init__(self):
self.MAX = 10000
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
def random_array(self):
import random
ret = list()
for i in self.UN_SORTED_ARRAY:
ret.append(i)
random.shuffle(ret)
return ret
# 希尔排序
def shell_sort(self):
array = self.random_array()
length = len(array)
# 默认初始分隔间距为length//2-1
# 可以通过修改初始分隔间隔,改善算法效率
increase = length//2
while increase > 0:
for i in range(0, increase):
# 分组偏移量
# *.....*.....*.. 偏移0
# .*.....*.....*. 偏移1
# ..*.....*.....* 偏移2
# ...*.....*..... 偏移3
# ....*.....*.... 偏移4
# .....*.....*... 偏移5
j = increase + i
while j < length:
val = array[j]
index = j
while index > 0:
if array[index-increase] > val:
array[index] = array[index-increase]
array[index-increase] = val
else:
break
index -= increase
j += increase
increase = increase//2
pass
return array
6. 归并排序
思想:化整为零,化零为整。将未排序序列S二分为更短序列,再将有序短序列合并为长序列。
过程:
维基百科总结了两种实现归并排序的方法:
一种:自顶而下二分分割再合并
另一种:将未排序序列视为一系列长度以2为底的序列集合,逐层自底而上合并
自顶而下分割:
- 二分法分割未排序长序列,直到无法继续分割(二叉树)
- 逆过程合并有序短序列(倒二叉树)
自底而上合并:
- i = 0,待排序序列S划分未相邻长度为2^i的序列
- 合并相邻两个子序列,并将合并后结果存于序列V
- 将一次计算结果V的数据存回S
- i++,若2^i未超过数组长度,回到1操作
时间复杂度:O(nlog2n)
空间复杂度:O(n)
优点:稳定排序,平均时间效率略逊快速(最坏情况优于快排最坏情况,最好情况逊于快排最好情况)
实现:
class Array:
def __init__(self):
self.MAX = 10000
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
def random_array(self):
import random
ret = list()
for i in self.UN_SORTED_ARRAY:
ret.append(i)
random.shuffle(ret)
return ret
# 归并排序
# 递归
# 子顶而下分割,再合并(Top-Down)
def merge_sort(self, array, n):
if n==1:
return array
m = n >> 1
left = self.merge_sort(array[0:m], m)
right = self.merge_sort(array[m:n], n-m)
ret = list()
i, j = 0, 0
while i<len(left) and j<len(right):
if left[i] < right[j]:
ret.append(left[i])
i += 1
else:
ret.append(right[j])
j += 1
pass
while i < len(left):
ret.append(left[i])
i+=1
while j < len(right):
ret.append(right[j])
j+=1
return ret
# 自底而上
# 将数组视为基于2的数组集合,两两合并
def merge_sort_buttom_up(self):
array = self.random_array()
length = len(array)
if length==1:
return array
ret = [0]*length
# flag = True
width = 1 # 子序列长度
while width < length:
for j in range(0, length, 2*width):
left, right = j, j+width
#若left + i越界
if left+width > length:
# 无任何操作,array序列为处理后结果,ret为上一个结果
#
# 设置状态,防止ret与序列交换
# flag = False
break
# 右边界可能出现越界问题
right_border = j+2*width
if right_border > length:
right_border = length
ret_index = j
while left<j+width and right<right_border:
if array[left] <= array[right]:
ret[ret_index] = array[left]
left += 1
else:
ret[ret_index] = array[right]
right += 1
ret_index += 1
pass
while left < j+width:
ret[ret_index] = array[left]
left += 1
ret_index += 1
while right < right_border:
ret[ret_index] = array[right]
right += 1
ret_index += 1
pass
# 深拷贝,将排序结果ret深拷贝到array
# self.copy_array(ret, array)
# if flag:
# 交换,不需要逐个复制
ret, array = array, ret
width *= 2
return array
# 将数组a拷贝到数组b
def copy_array(self, from_array, to_array):
# 直接拷贝复制
length = 0
# 若数组不等长,已短为基准
if len(from_array) > len(to_array):
length = len(to_array)
else:
length = len(from_array)
for i in range(0, length):
to_array[i] = from_array[i]
return
7. 快速排序
思想:未排序序列S中,若S[i],当0<k<i时,S[k] <= S[i],当k>i时,S[k]>=S[i],则当S升序排列后,S[i]刚好位于索引为i的位置上。
过程:
- 未排序序列S,起始位置start,结束位置end
- 一趟快排,找出S[start]在S中最终排序的位置pos
- 若pos > start+1,快速排序S[start:pos-1]
- 若pos < end -1,快速排序S[pos+1:end]
时间复杂度:O(nlog2n)
空间复杂度:O(1)
优点:平均性能优秀(平均性能优秀不代表任何时刻都优秀),空间代价低
缺点:不稳定(初始序列中,相同值前后顺序在排序后可能发生变化);若每趟排序定位出来的S[i]都位于序列首端/末端,性能严重下降(最差情况O(n2))。即(纯属个人猜测)快速排序对接近排序(数据基本有序,但是仍然未完全排序)的序列效率不高。
实现:
class Array:
def __init__(self):
self.MAX = 10000
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
def random_array(self):
import random
ret = list()
for i in self.UN_SORTED_ARRAY:
ret.append(i)
random.shuffle(ret)
return ret
def quick_sort(self):
array = self.random_array()
length = len(array)
stat = (0, length-1)
stat_list = list()
stat_list.insert(0, stat)
while len(stat_list) > 0:
stat = stat_list.pop()
left, right = stat
# 当前索引——初始为左边第一个
# 左指针之向下一个
current = left
while left < right:
# 从右开始扫描
# pos = right
while array[current] <= array[right] and left < right:
right -= 1
if left == right:
# 已找到current最终位置,跳出循环
break
array[right], array[current] = array[current], array[right]
current = right
left += 1
# 从左开始扫描
# pos = left
while array[current]>=array[left] and left < right:
left += 1
pass
if left == right:
# 已找到current最终位置,跳出循环
break
array[left], array[current] = array[current], array[left]
current = left
right -= 1
pass
# 已找到当前值array[current]位置
if current > stat[0]+1:
stat_list.insert(0, (stat[0], current-1))
if current < stat[1]-1:
stat_list.insert(0, (current+1, stat[1]))
pass
pass
return array
8. 桶排序
思想:少量数据排序代价总比大量数据排序代价低。假设数据满足均衡分布,将数据平均分到有序的几个桶中,在使用排序算法(不一定是桶排序)对每个桶内的数据进行排序。
过程:
- 设计桶的数目
- 指定合适规则,将数据分流到各个桶中
- 使用合适算法,对各个桶中数据进行排序
时间复杂度/空间复杂度:最好情况O(n),理论上,桶越多,桶排序时间性能越高,但是空间消耗越大。若数据分流规则不合理,导致数据集中流向少数几个桶中,也会造成时间性能的降低。
实现:
暂时空
9. 基数排序
思想:两个数比较大小,若是个为数相等,则十位数,若还相等,则比较百位……若按相同位数对未排序序列S进行分桶,再按桶顺序合并(桶采用队列)。当位数从最低位向高位变化,多次数据分流合流,最终数据将变为有序。(思想类似桶排序,或者说就是桶排序的一种实现)
过程:
- 待排序序列S,数据一共d位,数据按第i位进行分桶,第一次初始化i=1(后来不需要)
- 遍历序列S,将数据依次倒入各个桶(队列)
- 从小到大,将各个桶的数据倒回集合S(与倒入顺序一致)
- i++,若i<d,则继续1操作
时间复杂度:O(d*2n)(参考别人结论),由于多次进行分桶,时间性能比不上桶排序。
空间复杂度:O(i+k) k为桶数目
实现:
class Array:
def __init__(self):
self.MAX = 10000
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
def random_array(self):
import random
ret = list()
for i in self.UN_SORTED_ARRAY:
ret.append(i)
random.shuffle(ret)
return ret
# 基数排序
def radix_sort(self):
# array = self.random_array()
array = [10, 3, 2, 939, 29, 12, 93, 1, 2, 3, 99, 999, 8888]
d = 4 # 数据最大位数
buckets = [[] for i in range(10)]
prio = 1
for radix in range(0, d):
for data in array:
# 截取所在位数数字
index = data//prio
index = index % 10
buckets[index].insert(0, data)
pass
array.clear()
for bucket in buckets:
while len(bucket)>0:
array.append(bucket.pop())
prio *= 10
return array
10. 堆排序
思想:大项堆(二叉树结构 ,数组表示)根结点值大于子孙节点值。一个数组表示的大项堆,父节点索引dad,则左子节点son_left = dad*2+1,右子节点索引为son_right=dad*2+2。(不稳定)
过程:
- 将未排序序列S按顺序填入二叉树上(根结点0;第二层1,2;第三层3,4,5,6……
- 构造大项堆(……………待续……………)
- 将最后一个叶子节点的值与根结点(最值)互换,并将最后一个位置移出大项堆。将剩下二叉树部分重新变成大项堆
- 若所有数据移出二叉树,排序完成;否则继续3操作
时间复杂度:O(nlog2n),思考:一开始觉得构建大项堆时间效率与第一趟选择排序时间效率一样,不懂得堆排序比选择排序优秀在哪。经过比较,区别在于:当大项堆构建完成,取出第一个最值后,二叉树已经基本形成大项堆(除了根结点不符合大项堆,其余各个子树均是大项堆)。此时将二叉树重构成大项堆,只需要对二叉树其中一支分支遍历即可,不需要与剩余数据每一个进行比较(选择排序)获取最值
空间复杂度:O(n)
实现:
class Array:
def __init__(self):
self.MAX = 10000
self.UN_SORTED_ARRAY = [
10, 9, 8, 7, 6, 5, 4, 3, 2, 1
]
pass
def random_array(self):
import random
ret = list()
for i in self.UN_SORTED_ARRAY:
ret.append(i)
random.shuffle(ret)
return ret
def heap_sort(self):
array = self.random_array()
length = len(array)
for i in range(length//2-1, -1, -1):
self.max_headpify(array, i, length-1)
for i in range(length-1, -1, -1):
array[0], array[i] = array[i], array[0]
self.max_headpify(array, 0, i-1)
return array
def max_headpify(self, array, start, end):
dad = start
son = 2*dad+1
while son <= end:
if son+1 <= end and array[son] < array[son+1]:
son += 1
pass
if array[dad] >= array[son]:
return
else:
array[dad], array[son] = array[son], array[dad]
dad = son
son = dad * 2 + 1
pass
pass
11. 总结
基于比较算法:1 3 4 5 6 7 10
时间复杂度下限为O(nlog2n),空间复杂度:除了归并排序需要O(n),其他只需要若干辅助空间即可.
时间复杂度O(n2):1 3 4;O(nlog2n):5 6 7 10
稳定算法:1 3 4 6;不稳定:5 7 10
非比较算法:2 8 9
时间复杂度:O(n)
空间复杂度:非比较算法8 9需要额外O(n)辅助空间,2需要O(k)[k为整数范围].
8(桶排序)在分桶后各个桶可以采用基于比较的排序算法
稳定算法:2 9
桶排序是否稳定取决于分桶的规则和分桶后每个桶所采用的排序策略