分治策略:
- 分解(Divide):将问题划分为若干子问题
- 解决(Conquer):递归求解子问题
- 合并(combine):子问题组合成原问题
主方法:T(n) = aT(n/b)+f(n)
分解成a个问题,每个子问题降b倍,合并为O(f(n))
主定理:
比较f(n)和aT(n/b)的阶,要求是多项式意义上的比较,即只能差n^x。
1.f(n)的阶大,T就是O(f(n))
2.反之,为O[n^logb(a)] 正则条件:(对常数c<1,有af(n/b)<=cf(n))
3.最后就是O(f(n)) = O[n^logb(a)],O(T) = O[n^logb(a)*lgn]
n^logb(a)就是满a叉树中叶子节点的个数,对应递归树上,以上三种情况可以解释为:1.树的总代价由根节点决定 2.树的总代价由叶节点决定 3.树的总代价均匀分布在树的所有层次上
举个反例,不能使用主定理的情况:
T(n) = 2T(n/2)+nlgn
f(n)/n^logb(a) = nlgn/n = lgn 它是渐近小于n^c.
排序算法总结
排序的分类:
(一)内部排序和外部排序:内部排序就是在内存中排序;外部排序涉及数据量大,内存不够用,需要对外存访问的排序(多路归并排序)。
(二)基于比较和不基于比较的排序(基数排序)
还有一个稳定性的问题,若存在次关键字(不唯一),排序后位置变化就是不稳定的。
内部排序的分类(下面只标注时间复杂度不是O(n^2)的算法):
1.交换排序:冒泡排序(稳定),快速排序 (不稳定 O(nlgn) 空间复杂度:O(nlgn~n))
2.插入排序:直接插入排序(稳定), 折半插入排序(稳定),
希尔排序/缩小增量排序(不稳定)
3.选择排序:简单选择排序(不稳定),堆排序(不稳定 O(nlgn) ),二路归并排序(稳定 O(nlgn) 空间复杂度:O(n) )
4.不基于比较的基数排序(稳定 O(d(n+r))r:基数 n:节点数 d:每个节点关键字的位数
先介绍三个时间复杂度为O(n^2)的排序算法
冒泡,选择排序,插入排序
1.冒泡
def bubbleSort(alist):
n = len(alist)
for i in range(n-1, 0, -1):
for j in range(0, i):
if alist[j] > alist[j+1]:
alist[j], alist[j+1] = alist[j+1], alist[j]
return alist
# 改进的冒泡排序
def bubbleSort(alist):
n = len(alist)
exchange = False
for i in range(n-1, 0, -1):
for j in range(0, i):
if alist[j] > alist[j+1]:
alist[j], alist[j+1] = alist[j+1], alist[j]
exchange = True
# 如果发现整个排序过程中没有交换,提前结束
if not exchange:
break
return alist
2. 选择排序
它与冒泡的不同之处在于,需要一个多出变量存储遍历当前序列的最小值下标,每轮遍历只需要交换一次
def selectionSort(alist):
n = len(alist)
for i in range(n - 1):
# 寻找[i,n]区间里的最小值
min_index = i
for j in range(i+1, n):
if alist[j] < alist[min_index]:
min_index = j
alist[i], alist[min_index] = alist[min_index], alist[i]
return alist
#这是最简单的选择排序,另外还有堆排序
3. 插入排序
它是将新元素插入已排好序的序列中,同样需要存储排好序的队尾元素下标,找到第一个比它小的位置之前插入,关键就是找到比currentvalue小的元素的位置,比currentvalue大就后移。它是需要比较和移动的。
移动次数会比选择排序多,但是它大大减少了比较次数,只要在已经基本有序的序列中找到位置就停止比较和移动了。
def insertionSort(alist):
for i in range(1,len(alist)):
currentvalue=alist[i]
position=i
while alist[position-1]>currentvalue and position>0:
alist[position]=alist[position-1]
position=position-1
alist[position]=currentvalue
return alist
希尔排序
#基于插入排序的性能提升:需要待排记录基本有序和n值较小,可以大大减少移动比较次数。(如果序列已经有序,只需要比较n-1次)
#增量序列函数有很多,没有最好,它的好坏直接影响时间复杂度。
# 希尔排序
def shellSort(alist):
n = len(alist)
gap = n // 2
#" / "就表示 浮点数除法,返回浮点结果;" // "表示整数除法,代表不大于n/2的整数
while gap > 0:
for i in range(gap): #每轮gap个子列,gap//2产生子列的函数
gapInsetionSort(alist, i, gap) #每个子列分别调直接插入排序
gap = gap // 2
return alist
# # start子数列开始的起始位置, gap表示间隔
def gapInsetionSort(alist,startpos,gap):
#希尔排序的辅助函数
for i in range(startpos+gap,len(alist),gap):
#每轮有gap个子序列,每个子列有len(alist)/gap个元素,
position=i
currentvalue=alist[i]
while position>startpos and alist[position-gap]>currentvalue:
alist[position]=alist[position-gap]
position=position-gap
alist[position]=currentvalue
下面介绍使用分治法的经典算法,快速排序
快速排序
快排充分体现了分治法的思想,运用递归,每次将待排序列一分为二,最终分到不可分。
就是三个循环只需要一个变量的额外存储空间,选取枢轴量,空出一个元素,然后“左右互搏”,左右指针从序列两端分别遍历,交换元素。快排需要一个栈空间实现递归,深度为lgn~n
改进:
(1)随机化改进:不是选取第一个值为基准,而是随机选取。
平衡化改进:取第一个、最后一个和中间点三个值中中间值为基准进行排序。
设置阀值–混合排序:当数组长度小于某一值时使用其他较快的排序。
(2)可以改进版的冒泡+快速排序,即在左右指针向中间移动时,同时执行冒泡排序,如果碰头时没有交换,证明该半边已经基本有序,那么就不需要再操作该半边元素。
(3)每次只对元素较少的半边进行快排
1.
def quick_sort(nums, start, end):
if start >= end:
return
pivot = nums[start] # 基准值
low = start # 左指针
high = end # 右指针
while low < high:
while low < high and nums[high] >= pivot:
high -= 1
nums[low] = nums[high]
while low < high and nums[low] <= pivot:
low += 1
nums[high] = nums[low]
nums[low] = pivot
quick_sort(nums, start, low - 1)
quick_sort(nums, low + 1, end)
nums = [1,3,54, 26, 93, 44,17, 77, 31, 1,321,44, 55, 20]
quick_sort(nums, 0, len(nums) - 1)
print(nums)
#结果:
[1, 1, 3, 17, 20, 26, 31, 44, 44, 54, 55, 77, 93, 321]
2.廖雪峰版本
from random import Random
def quick_sort(arr):
if len(arr) > 1:
qsort(arr, 0, len(arr) - 1)
def qsort(arr, start, end):
base = arr[start]
pl = start
pr = end
while pl < pr:
while pl < pr and arr[pr] >= base:
pr -= 1
if pl == pr:
break
else:
arr[pl], arr[pr] = arr[pr], arr[pl]
while pl < pr and arr[pl] <= base:
pl += 1
if pl == pr:
break
else:
arr[pl], arr[pr] = arr[pr], arr[pl]
# now pl == pr
if pl - 1 > start:
qsort(arr, start, pl - 1)
if pr + 1 < end:
qsort(arr, pr + 1, end)
r = Random()
a = []
for i in range(20):
a.append(r.randint(0, 100))
print a
quick_sort(a)
print a
#结果:
[40, 57, 29, 21, 98, 37, 16, 77, 63, 86, 63, 12, 23, 68, 38, 66, 64, 13, 63, 36]
[12, 13, 16, 21, 23, 29, 36, 37, 38, 40, 57, 63, 63, 63, 64, 66, 68, 77, 86, 98]
如果设f(n)为数组长为n时的比较次数,则f(n)=[(f(1)+f(n-1))+(f(2)+f(n-2))+…+(f(n-1)+f(1))]/n.
利用数学知识易知f(n)=(n+1)*[1/2+1/3+…+1/(n+1)]-2n~1.386nlog(n).
归并排序
归并也体现分治的思想
详见这篇博客
# 归并排序
def mergesort(seq):
"""归并排序"""
if len(seq) <= 1:
return seq
mid = len(seq) / 2 # 将列表分成更小的两个列表
# 分别对左右两个列表进行处理,分别返回两个排序好的列表
left = mergesort(seq[:mid])
right = mergesort(seq[mid:])
# 对排序好的两个列表合并,产生一个新的排序好的列表
return merge(left, right)
def merge(left, right):
"""合并两个已排序好的列表,产生一个新的已排序好的列表"""
result = [] # 新的已排序好的列表
i = 0 # 下标
j = 0
# 对两个列表中的元素 两两对比。
# 将最小的元素,放到result中,并对当前列表下标加1
while i < len(left) and j < len(right):
if left[i] <= right[j]:
result.append(left[i])
i += 1
else:
result.append(right[j])
j += 1
result += left[i:]
result += right[j:]
return result
自底向上(非递归法)方法
# 自底向上的归并算法
def mergeBU(alist):
n = len(alist)
#表示归并的大小
size = 1
while size <= n:
for i in range(0, n-size, size+size):
merge(alist, i, i+size-1, min(i+size+size-1, n-1))
size += size
return alist
# 合并有序数列alist[start....mid] 和 alist[mid+1...end],使之成为有序数列
def merge(alist, start, mid, end):
# 复制一份
blist = alist[start:end+1]
l = start
k = mid + 1
pos = start
while pos <= end:
if (l > mid):
alist[pos] = blist[k-start]
k += 1
elif (k > end):
alist[pos] = blist[l-start]
l += 1
elif blist[l-start] <= blist[k-start]:
alist[pos] = blist[l-start]
l += 1
else:
alist[pos] = blist[k-start]
k += 1
pos += 1