文章目录
Teil 2 第二部分_排序和顺序统计量
Chapter 6 堆排序
6.1 堆
如下图所示的一个数组:
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|
16 | 14 | 10 | 8 | 7 | 9 | 3 | 2 | 4 | 1 |
那我们其实不难得出父节点. 左孩子和右孩子的下标之间的关系:
PARENT(i)
return floor(i/2)
LEFT(i)
return 2*i
RIGHT(i)
return 2*i+1
最大堆: 除了根以外的所有结点 i i i都要满足: A[PARENT(i)]>=A[i]. 不难得出: 最大堆的最大元素存放在根结点中.
类似地:
最小堆: 除了根以外的所有结点 i i i都要满足: A[PARENT(i)]<=A[i]. 同样地: 最小堆的最小元素存放再根结点中.
某个结点的高度: 该结点到叶结点最长简单路径上边的数目
6.2 维护堆的性质
MAX-HEAPIFY是用于维护最大堆性质的重要过程. 它的输入为一个数组A和一个下标i. 再调用MAX-HEAPIFY的时候, 我们假定根结点为LEFT(i)和RIGHT(i)的二叉树都是最大堆, 但是这时A[i]有可能小于其孩子, 这样就违背了最大堆的性质. MAX-HEAPIFY通过让A[i]的值再最大堆中"逐级下降", 从而使得以下标i为结点的子树重新遵循最大堆的性质.
MAX-HEAPIFY(A, i)
l = LEFT(i)
r = RIGHT(i)
if l<=A.heap-size and A[l]>A[i]
largest = l
else largest = i
if r<=A.heap-size and A[r]>A[largest]
largest = r
if largest != i
exchange A[i] with A[largest]
MAX-HEAPIFY(A, largest)
考虑有一个最底层半满的子树, 每一层的元素为:
2
0
,
2
1
,
2
2
,
⋯
,
2
i
,
2
i
2^0, 2^1, 2^2, \cdots, 2^i, 2^i
20,21,22,⋯,2i,2i
求和的结果为:
2
i
+
1
+
2
i
−
1
2^{i+1}+2^i-1
2i+1+2i−1
经过迭代以后的子数每层元素为:
2
0
,
2
1
,
2
2
,
⋯
,
2
i
2^0, 2^1, 2^2, \cdots, 2^i
20,21,22,⋯,2i
求和的结果为: 2 i + 1 − 1 2^{i+1}-1 2i+1−1
这样一来, 前后两次的元素个数之比约为:
lim
i
→
∞
2
i
+
1
−
1
2
i
+
1
+
2
i
−
1
≈
2
3
\lim\limits_{i\to \infty}{2^{i+1}-1\over 2^{i+1}+2^i-1}\approx {2\over 3}
i→∞lim2i+1+2i−12i+1−1≈32
由此不难得出:
T
(
n
)
≤
T
(
2
n
/
3
)
+
Θ
(
1
)
T(n)\le T(2n/3)+\Theta(1)
T(n)≤T(2n/3)+Θ(1)
根据主定理, 这个递归式的解为 T ( n ) = O ( lg n ) T(n)=O(\lg n) T(n)=O(lgn). 换句话来说, 对于一个树高为h的结点来说, MAX-HEAPIFY的时间复杂度是 O ( h ) O(h) O(h).
练习
不使用递归算法:
def max_heapify2(self, i):
"""
不使用递归而采用循环的方法来实现max_heapify
"""
loop = i
while loop < int(self.heap_size()/2):
l = self.left(loop)
r = self.right(loop)
if l<=self.heap_size() and self.nums[l]>self.nums[loop]:
largest = l
else:
largest = loop
if r<=self.heap_size() and self.nums[r]>self.nums[largest]:
largest = r
if largest != loop:
self.exchange(loop, largest)
if loop == largest:
return 0
loop = largest
6.3 建堆
我们只要从下向上地执行MAX-HEAPIFY程序, 就可让一个数组转换为最大堆.
BUILD-MAX-HEAP(A)
A.heap-size = A.length
for i =[A.length/2] downto 1
MAX-HEAPIFY(A, i)
分析这个算法, 包含
n
n
n个元素的堆的高度为
⌊
lg
n
⌋
\lfloor \lg n \rfloor
⌊lgn⌋; 高度为
h
h
h的堆最多包含
⌈
/
2
h
+
1
⌉
\lceil / 2^{h+1}\rceil
⌈/2h+1⌉个结点, 考虑到在一个高度为
h
h
h的结点上运行MAX-HEAPIFY的代价是
O
(
h
)
O(h)
O(h), 由此BUILD-MAX-HEAP的总代价可以表示为:
∑
h
=
0
⌊
lg
n
⌋
⌈
n
2
h
+
1
⌉
O
(
h
)
=
O
(
n
∑
h
=
0
⌊
lg
n
⌋
h
2
h
)
\sum^{\lfloor \lg n \rfloor}_{h=0}\left \lceil {n \over 2^{h+1}} \right \rceil O(h)= O\left(n\sum^{\lfloor \lg n \rfloor}_{h=0}{h\over 2^h}\right)
h=0∑⌊lgn⌋⌈2h+1n⌉O(h)=O⎝⎛nh=0∑⌊lgn⌋2hh⎠⎞
对之取极限, 不难得到最终的结论为
O
(
n
)
O(n)
O(n).
6.4 堆排序算法
HEAPSORT(A)
BUILD-MAX-HEAP(A)
for i = A.length downto 2
exchange A[1] with A[i]
A.heap-size = A.heap-size - 1
MAX-HEAPIFY(A, 1)
下面记录一下实现堆排序算法的类以及实现Young式数组的类:
class my_tree:
def __init__(self, nums):
self.nums = nums
self.heap_size = len(nums)-1
def exchange(self, i, j):
# 交换二叉树两结点的位置
val = self.nums[i]
self.nums[i] = self.nums[j]
self.nums[j] = val
def parent(self,i):
# 返回父结点的下标
return int((i+1)/2)-1
def left(self,i):
# 返回左孩子的下标
return 2*i+1
def right(self,i):
# 返回右孩子的下标
return 2*(i+1)
def judgePosition(self, length):
"""
根据位置判断此数据应该在二叉树的第几行、第几列
:param length:
:return:
"""
rows = 0
# get rows
while True:
if (2 ** (rows - 1)) <= length <= (2 ** rows - 1):
break
else:
rows += 1
# get cols
cols = length - 2 ** (rows - 1) + 1
return rows, cols
def binaryTreePrinter(self, listOne=None):
"""
打印列表为二叉树形状
:param listOne: 传入的列表
:return:
"""
listOne = self.nums
rows = self.judgePosition(len(listOne))[0] # rows为总行数
count = 0
pre_row, pre_col = 0, 0 # 记录上一个数的行与列
for x in listOne:
count += 1 # 计数现在打印的是第几个数
x_row, x_col = self.judgePosition(count) # 计算出当前打印的数位于第几行,第几列
if x_row != pre_row: # 如果换行了,那么打印换行符
print('\n')
if x_col != 1: # 如果当前打印的数字不是本行第一个,则打印步长为2**(rows-x_row+1)-1
print(' ' * (2 ** (rows - x_row + 1) - 1), end='')
else:
print(' ' * ((2 ** (rows - x_row + 1) - 1) // 2), end='')
print('{0:2}'.format(x), end='')
pre_row, pre_col = x_row, x_col # 位置记录更新
def max_heapify(self, i):
"""
我们假定i位置的结点, 他的根结点为left(i)和right(i)的二叉树都有最大堆
利用这个函数我们就可以让根结点为i的二叉树也满足最大堆
"""
l = self.left(i)
r = self.right(i)
if l<=self.heap_size and self.nums[l]>self.nums[i]:
largest = l
else:
largest = i
if r<=self.heap_size and self.nums[r]>self.nums[largest]:
largest = r
if largest != i:
self.exchange(i, largest)
self.max_heapify(largest)
def max_heapify2(self, i):
"""
不使用递归而采用循环的方法来实现max_heapify
"""
loop = i
while loop < int(self.heap_size/2):
l = self.left(loop)
r = self.right(loop)
if l<=self.heap_size and self.nums[l]>self.nums[loop]:
largest = l
else:
largest = loop
if r<=self.heap_size and self.nums[r]>self.nums[largest]:
largest = r
if largest != loop:
self.exchange(loop, largest)
if loop == largest:
return 0
loop = largest
def build_max_heap(self):
"""
将任意一个数组整理成一个最大堆, 实现的方法就是从self.heap_size/2)的位置开始
向上采用max_heapify整理
"""
for i in range(int(self.heap_size/2),-1,-1):
self.max_heapify(i)
def heapsort(self):
"""
使用堆排序堆数组进行排序
"""
self.build_max_heap() # 首先建立一个最大堆
for i in range(len(self.nums)-1,0,-1):
"""
从最后一位向前开始循环
"""
self.exchange(0, i) # 每次循环开始的时候, 在heap_size之前都是最大堆
# 由此可得, 第一位的元素一定是这些元素里最大的
self.heap_size = self.heap_size - 1
self.max_heapify(0) # 在经过交换后, 已经将i位置上比较小的元素和0位置上的元素进行了交换, 这个时候需要重新再建立一次最大堆, 并且缩小建堆的范围.
class young_matrix:
def __init__(self,matrix = [], nums=[]):
self.nums = nums
if matrix == []:
n = math.ceil(math.sqrt(len(nums)))
self.matrix = np.array([[float('inf')]*n]*n)
self.endi = n - 1
self.endj = n - 1
else:
self.matrix = matrix
self.endi = len(matrix)-1
self.endj = len(matrix[0])-1
def defyoung(self,starti=0, startj=0):
# 判断一个矩阵是否是Young氏矩阵, 一个元素和右边以及下边的元素作比较
for i in range(starti, self.endi):
for j in range(startj, self.endj):
# 去掉最外面一圈边, 来看这些元素是否满足
if self.matrix[i][j]>self.matrix[i][j+1] or self.matrix[i][j]>self.matrix[i+1][j]:
return False
j = self.endj
for i in range(starti, self.endi):
# 判断最底下一行
if self.matrix[i][j]>self.matrix[i+1][j]:
return False
i = self.endi
for j in range(startj, self.endj):
# 判断最右一列
if self.matrix[i][j]>self.matrix[i][j+1]:
return False
return True
def exchange(self,oldi, oldj, newi, newj):
# 交换矩阵中的两个元素
val = self.matrix[oldi][oldj]
self.matrix[oldi][oldj] = self.matrix[newi][newj]
self.matrix[newi][newj] = val
def extract_young(self, i=0, j=0):
"""
让i, j向下右延伸的矩阵称为Young氏矩阵
注意, 我们认为i+1, j和i, j+1处的矩阵已经是Young式矩阵了
"""
if self.defyoung(i, j):
return 0
elif i == self.endi:
self.exchange(i,j,i,j+1)
newi = i
newj = j+1
elif j == self.endj:
self.exchange(i,j,i+1,j)
newi = i+1
newj = j
elif self.matrix[i+1][j]<self.matrix[i][j+1]:
self.exchange(i,j,i+1,j)
newi = i+1
newj = j
else:
self.exchange(i,j,i,j+1)
newi = i
newj = j+1
self.extract_young(newi, newj)
def insert_young(self, i=None, j=None, num=None):
'''
假定i-1,j和i,j-1处的矩阵已经是Young式矩阵, 让i, j处的元素融入其中
'''
if i == None or j == None:
i = self.endi
j = self.endj
if num != None:
self.matrix[i][j] = num
if i == 0 and j == 0:
return 0
elif i == 0 and self.matrix[i][j-1]<self.matrix[i][j]:
return 0
elif j == 0 and self.matrix[i-1][j]<self.matrix[i][j]:
return 0
elif self.matrix[i-1][j]<self.matrix[i][j] and self.matrix[i][j-1]<self.matrix[i][j]:
return 0
elif i == 0:
newi = i
newj = j - 1
self.exchange(i, j, newi, newj)
self.insert_young(newi,newj)
elif j == 0:
newi = i - 1
newj = j
self.exchange(i, j, newi, newj)
self.insert_young(newi,newj)
elif self.matrix[i-1][j]<self.matrix[i][j-1]:
newi = i
newj = j - 1
self.exchange(i, j, newi, newj)
self.insert_young(newi,newj)
else:
newi = i - 1
newj = j
self.exchange(i, j, newi, newj)
self.insert_young(newi,newj)
def arrange(self):
"""
把self.nums数组一个一个地放到self.matrix中, 使之成为一个young氏矩阵
"""
n = len(self.nums)
for i in range(0, n):
self.matrix[self.endi][self.endj] = self.nums[i]
self.insert_young()
def extract(self):
"""
把self.matrix矩阵中的元素, 一个一个取出来, 组成一个经过排序的self.nums数组
"""
n = len(self.nums)
for i in range(0, n):
self.nums[i] = self.matrix[0][0]
self.matrix[0][0] = float('inf')
self.extract_young()
Chapter 7 快速排序
对于包含 n n n个数的输入数组来说, 快速排序是一种最坏情况时间复杂度为 Θ ( n 2 ) \Theta(n^2) Θ(n2)的排序算法, 不过它的期望时间复杂度是 Θ ( n lg n ) \Theta(n\lg n) Θ(nlgn), 而且它的隐含常数因子非常小.
7.1 快速排序的描述
先展示一下伪代码:
QUICKSORT(A, p, r)
if p<r
q = PARTITION(A, p, r)
QUICKSORT(A, p, q-1)
QUICKSORT(A, q+1, r)
PARTITION(A, p, r)
x = A[r]
i = p - 1
for j = p to r - 1
if A[j]<=x
i = i + 1
exchange A[i] with A[j]
exchange A[i+1] with A[r]
return i + 1
利用python实现以上两段伪代码:
def exchange(A, i, j):
val = A[i]
A[i] = A[j]
A[j] = val
def partition(A, p = None, r = None):
if p == None or r == None:
p = 0
r = len(A)-1
x = A[r] # 把输入的数组的最后一个元素作为比较判断元素
i = p - 1
for j in range(p, r):
if A[j]<=x:
i = i + 1
exchange(A, i, j)
exchange(A, i + 1, r)
return i + 1
def quicksort(A, p = None, r = None):
if p == None or r == None:
p = 0
r = len(A)-1
if p < r:
q = partition(A, p, r)
quicksort(A, p, q - 1)
quicksort(A, q + 1, r)
来分析一下quicksort(A, p, r)程序:
分解: 把数组分成A[p…q-1]和A[q+1…r], 使得左边的数组中的每一个元素都小于A[q]小于右边的数组中的每个元素.
解决: 递归调用quicksort(A, p, r)对左右两个数组进行排序
合并: 原址排序, 不必合并.
来解释一下PARTITION的实现原理:
比如我们处理一个如下的数组:
A = [28 84 98 66 18 1 32 32]
我们执行partition(A, 0, 7)
最后的元素32是分界元素, 然后每走一步, 得到的结果如下:
[28 84 98 66 18 1 32 32]
[28 84 98 66 18 1 32 32]
[28 84 98 66 18 1 32 32]
[28 84 98 66 18 1 32 32]
[28 84 98 66 18 1 32 32]
[28 18 98 66 84 1 32 32]
[28 18 1 66 84 98 32 32]
[28 18 1 32 32 98 66 84]
循环不变量:
- 若p<=k<=i, 则A[k]<=x
- 若i+1<=k<=j-1, 则A[k]>x
- 若k=r, 则A[k]=x
初始化: 在迭代以前, i = p - 1, j = p. p和i之间的大小导致, 不存在元素, i+1和j-1之间也是同理.
保持: 如果说A[j]>x那么这个元素就不应该移动, 继续保持在原来的位置, 如果A[j]<=x, 那么就把他加到数组A[p:i]的最后, 让A[j]和在经历过i+1以后的A[i]交换位置.
终止: 到最后, j = r, 把A[r]和A[i+1]互相交换, 让主元到它该到的地方去.
7.2 快速排序的性能
快速排序的运行时间依赖于划分是否平衡, 而这又依赖于划分的元素(主元).
最坏情况划分
最糟糕的情况是两个子问题分别包含了n-1个元素和0个元素. 我们不妨假设每次都是这样:
T
(
n
)
=
T
(
n
−
1
)
+
T
(
0
)
+
Θ
(
n
)
=
T
(
n
−
1
)
+
Θ
(
n
)
T(n) = T(n-1) + T(0) + \Theta(n) = T(n-1) + \Theta(n)
T(n)=T(n−1)+T(0)+Θ(n)=T(n−1)+Θ(n)
这种情况下:
T
(
n
)
=
Θ
(
n
2
)
T(n)=\Theta(n^2)
T(n)=Θ(n2)
最好情况划分
如果每次都是均匀划分, 那么:
T
(
n
)
=
2
T
(
n
/
2
)
+
Θ
(
n
)
T(n)=2T(n/2)+\Theta(n)
T(n)=2T(n/2)+Θ(n)
那么根据我们所学过的知识:
T
(
n
)
=
Θ
(
n
lg
n
)
T(n)=\Theta(n\lg n)
T(n)=Θ(nlgn)
平衡的划分
我们姑且考虑一个9: 1的划分, 他的递归式就是:
T
(
n
)
=
T
(
9
n
/
10
)
+
T
(
n
/
10
)
+
c
n
T(n)=T(9n/10)+T(n/10)+cn
T(n)=T(9n/10)+T(n/10)+cn
我们分析 1 10 n {1\over10}n 101n这一个分支, 最快到达这一支的叶其实只需要 log 10 n \log_{10}n log10n层即可.
而对于 9 10 n {9\over 10}n 109n这一分支, 最慢到达它的叶则需要 log 10 / 9 n \log _{10/9}n log10/9n层.
对于 1 ∼ log 10 n 1\sim\log_{10}n 1∼log10n层, 每一层都需要花费 c n cn cn的时间, 而从下开始一直发展到最后, 花费的时间则小于 c n cn cn. 由此不难得出, 这个算法的渐近上界是: O ( n lg n ) O(n\lg n) O(nlgn)
7.3 快速排序的随机化版本
为了放置我们排序的数组比较有规律, 多次出现主元是较大或者较小的元素而发生对排序不利的情况发生, 我们可以在排序的时候随机选择主元开始排序, 具体的伪代码如下:
RANDOMIZED-PARTITION(A, p, r)
i = random(p, r)
exchange A[r] with A[i]
return PARTITION(A, p, r)
RANDOMIZED-QUICKSORT(A, p, r)
if p<r
q = RANDOMIZED-PARTITION(A, p, r)
RANDOMIZED-QUICKSORT(A, p, q - 1)
RANDOMIZED-QUICKSORT(A, q + 1, r)
7.4 快速排序分析
7.4.1 最坏情况分析
用代入法不难证明这是一个 Θ ( n 2 ) \Theta(n^2) Θ(n2)的算法
7.4.2 期望运行时间
引理7.1 当在一个包含 n n n个元素的数组上运行QUICKSORT时, 假设在PARTITION的第4行中所做比较的次数为 X X X, 那么QUICKSORT的运行时间为 O ( n + x ) O(n+x) O(n+x).
每执行一次PARTITION, 数组中就有一个元素的位置被确定了, 那么 算法其实最多对PARTITION调用 n n n次. 对于PARTITION的每一次调用都包括一个固定的故作两和执行若干次for循环. 我们只要知道for循环, 也就是PARTITION的第4行如何执行多少次就好了.
我们将数组A的各个元素重新命名为 z 1 , z 2 , ⋯ , z n z_1, z_2,\cdots,z_n z1,z2,⋯,zn 定义 Z i j Z_{ij} Zij为 z i z_i zi到 z j z_j zj之间的元素集合.
为了便于讨论我们把PARTITION的伪代码粘贴如下:
PARTITION(A, p, r)
x = A[r]
i = p - 1
for j = p to r - 1
if A[j]<=x
i = i + 1
exchange A[i] with A[j]
exchange A[i+1] with A[r]
return i + 1
我们可以看到, 只有当两个元素进行比较的时候, 才有可能执行第四行的if程序. 事实上并不是任意两个元素之间都会进行比较, 例子在书102页上有所阐述.
仔细分析一下可以知道,
z
i
z_i
zi和
z
j
z_j
zj比较的条件是
z
i
z_i
zi和
z
j
z_j
zj中的一个元素被选为了主元.
Pr
{
z
i
与
z
j
进
行
比
较
}
=
Pr
{
z
i
或
z
j
是
集
合
Z
i
j
中
选
出
的
第
一
个
主
元
}
=
Pr
{
z
i
是
集
合
Z
i
j
中
的
第
一
个
主
元
}
+
Pr
{
z
j
是
集
合
Z
i
j
中
的
第
一
个
主
元
}
=
1
j
−
i
+
1
+
1
j
−
i
+
1
=
2
j
−
i
+
1
\begin{aligned} \Pr\{z_i与z_j进行比较\}&=\Pr\{z_i或z_j是集合Z_{ij}中选出的第一个主元\} \\&=\Pr\{z_i是集合Z_{ij}中的第一个主元\}+\Pr\{z_j是集合Z_{ij}中的第一个主元\} \\&={1\over j-i+1}+{1\over j-i+1}={2\over j-i+1} \end{aligned}
Pr{zi与zj进行比较}=Pr{zi或zj是集合Zij中选出的第一个主元}=Pr{zi是集合Zij中的第一个主元}+Pr{zj是集合Zij中的第一个主元}=j−i+11+j−i+11=j−i+12
任何一个元素被选成了主元其实都是一个完全的随机事件. 下面我们只要让
i
,
j
i, j
i,j遍历
p
∼
r
p\sim r
p∼r中的每一个元素就好了.
E
[
X
]
=
∑
i
=
1
n
−
1
∑
j
=
i
+
1
n
2
j
−
i
+
1
\mathrm{E} [X]=\sum^{n-1}_{i=1}\sum^n_{j=i+1}{2\over j-i+1}
E[X]=i=1∑n−1j=i+1∑nj−i+12
下面开始一波操作, 最后可以证明:
E
[
X
]
=
O
(
n
lg
n
)
\mathrm{E}[X]=O(n\lg n)
E[X]=O(nlgn).
Chapter 8 线性时间排序
8.1 排序算法的下界
决策树模型
我们可以注意到, 每一个叶节点上都对应了一中序列. 排序算法的执行对应于一条从数的根节点到叶节点的路径.
最坏情况的下界
决策树中, 从根结点到任意一个可达叶结点之间的最长简单路径的长度, 表示的是对应的排序算法中最坏情况下的比较次数. 根据决策树模型我们不难理解如下定理
定理8.1 在最坏情况下, 任何比较排序算法都需要做 Ω ( n lg n ) \Omega(n\lg n) Ω(nlgn)次比较.
8.2 计数排序
计数排序: 假设 n n n个输入元素中的每一个都是在 0 0 0到 k k k区间内的一个整数, 其中 k k k为某个整数. 当 k = O ( n ) k=O(n) k=O(n)时, 排序的运行时间为 Θ ( n ) \Theta(n) Θ(n).
COUNTING-SORT(A, B, k)
let C[0..k] be a new array
for i = 0 to k
C[i] = 0
for j = 1 to A.length
C[A[j]] = C[A[j]] + 1
//C[i] now contains the number of elements equal to i.
for i = 1 to k
C[i] = C[i] + C[i-1]
//C[i] now contains the number of elements less than or equal to i.
for j = A.length downto 1
B[C[A[j]]] = A[j]
C[A[j]] = C[A[j]] - 1
利用python可以这样实现:
def counting_sort(A, min, max):
n = max - min + 1 # 计算数组A中上限和下限之间一共有多少整数
C = np.zeros(n) # 按照整数个数, 一个萝卜一个坑, 创建计数数组C
B = A.copy() # 创建一个临时数组B用于储存A的排序结果
for a in A:
a = a - min # 调整一下下标的数字
C[a] = C[a] + 1 # C中的数字从小到大, 对应了A中的数字从小到大的个数
for i in range(1, n):
C[i] = C[i] + C[i-1] # 计算某一位之前拥有的数字的个数
for i in range(0, len(A)):
B[int(C[A[i]-min])-1] = A[i] # 考虑到python数组从0开始, 这里需要-1
C[A[i]-min] = C[A[i]-min] - 1 # 填了一个萝卜了, 就减去一个萝卜
A.copy()
del B
return A
计数排序的一个重要的性质就是他是稳定的: 具有相同值的元素在输出数组中的相对次序与他们在输入数组中的相对次序相同.
练习8.2
8.2-4: 设计一个算法, 能够对于任何给定的介于0到k之间的n个整数先进行预处理, 然后在O(1)时间内回答输入的n个整数中有多少个落在区间[a…b]内.
def counting_num(A, min, max, low, high):
n = max - min + 1 # 计算数组A中上限和下限之间一共有多少整数
C = np.zeros(n) # 按照整数个数, 一个萝卜一个坑, 创建计数数组C
for a in A:
a = a - min # 调整一下下标的数字
C[a] = C[a] + 1 # C中的数字从小到大, 对应了A中的数字从小到大的个数
for i in range(1, n):
C[i] = C[i] + C[i-1] # 计算某一位之前拥有的数字的个数
low = low - min
high = high - min
if low == 0:
return C[high]
if low != 0:
return C[high]-C[low-1]
8.2 基数排序
我们研究下面这个算法:
RADIX-SORT(A, d)
for i = 1 to d
use a stable sort to sort array A on digit i
引理8.3 给定n个d位数, 其中每一个数位都有k个可能的取值. 如果RADIX-SORT使用的稳定排序方法耗时 Θ ( n + k ) \Theta(n+k) Θ(n+k), 那么它就可以在 Θ ( d ( n + k ) ) \Theta(d(n+k)) Θ(d(n+k))时间内将这些数排好序.
引理8.4 给定 n n n个 b b b位数和任何正整数 r ≤ b r\le b r≤b, 如果RADIX-SORT使用的稳定排序算法对数据取值区间是0到k的输入进行排序排序耗时 Θ ( n + k ) \Theta(n+k) Θ(n+k), 那么他就可以在 Θ ( ( b / r ) ( n + 2 r ) ) \Theta((b/r)(n+2^r)) Θ((b/r)(n+2r))时间内将这些数排好序.
喝多了, 有点看不懂, 以后有兴趣再看.
8.4 桶排序
我们先看一下如下的伪代码:
BUCKET-SORT(A)
n = A.length
let B[0..n-1] be a new array
for i = 0 to n - 1
make B[i] an empty list
for i = 1 to n
insert A[i] into list B[floor(n*A[i])]
for i to n-1
sort list B[i] with insertion sort
concatenate the list B[0], B[1], ..., B[n-1] together in oder
书上说, 这里需要用链表来实现, 我就姑且先用数组来做了.
这里照例说应该还有两页的练习题, 有兴趣再看吧
Chapter 9 中位数和顺序统计量
在本章节中我们将研究如下问题:
输入: 一个包含
n
n
n个(互异的)数的集合
A
A
A和一个整数
i
i
i,
1
≤
i
≤
n
1\le i\le n
1≤i≤n.
输出: 元素
x
∈
x\in
x∈A, 且
A
A
A中恰好有
i
−
1
i-1
i−1个其他元素小于它.
根据我们之前所学的关于排序算法的结论, 我们自然可以在
O
(
n
lg
n
)
O(n\lg n)
O(nlgn)时间内解决这个问题, 只需要把排序之后找到下标第
i
i
i个元素即可, 但是事实上我们可以有更快的方法.
9.1 最大值和最小值
我们观察如下的算法:
Minimum(A)
min = A[1]
for i = 2 to A.length
if min>A[i]
min = A[i]
当然最大值也可以通过同样的方法在 n − 1 n-1 n−1次比较来找出来. 对于寻找最小值的问题, 这显然已经是最好的算法了.
同时找到最小值和最大值
我们在确定了最大值和最小值了以后, 同时选取两个元素出来开始进行比较, 它们自己和自己比完以后, 大的和最大值比较, 小的和最小值比较.这样两个元素就只需要三次比较就可以实现同时确定最大值和最小值了, 所以最终的运行时间应该时在 3 n / 2 3n/2 3n/2这个量级上.
练习9.1
在最坏情况下, 找到
n
n
n个元素中第二小的元素需要
n
+
⌈
lg
n
⌉
−
2
n+\lceil \lg n\rceil-2
n+⌈lgn⌉−2次比较
我们可以将数组中的元素进行两两比较并且同时建立起一颗二叉树, 进行完成第一轮比较之后我们可以找出最小的元素, 这需要花费的代价是
n
−
1
n-1
n−1, 在这之后我们再将所有和最小的元素比较过的元素揪出来, 找到其中最小的, 一个有
lg
n
\lg n
lgn个, 这样就可以解得结果了.
9.2 期望为线性时间的选择算法
利用之前的快速排序算法中的partition程序, 我们可以知道, 我们可以在n的时间内, 确定某一个元素它在这个数组中应该在的顺序统计量的位置, 那么利用这一性质, 我们其实可以以下面这个算法很快的确定我们需要寻找的数字:
RANDOMIZED-SELECT(A, p, r, i)
if p==r
return A[p]
q = RANDOMIZED-PARTITION(A, p, r)
k = q - p + 1
if i==k
return A[q]
else if i<k
return RANDOMIZE-PARTITION(A, p, q-1, i)
else return RANDOMIZED-SELECT(A, q+1, r i-k)
不过说起来, 我不是很明白为什么书上这里会考虑在每一次输入函数的i, 是在考虑p和r的实际值移位之后的. 我实现这段伪代码时写的Python脚本就没有采用书上的这种方法, 而是直接采用绝对的下标i:
def partition(A, p, r):
"""
将数组中的某一个元素放到它所应该在的顺序统计位置上
这partition和c7当中不同的是, 它已经在函数内部包含了随机化的步骤
"""
i = random.randint(p, r)
exchange(A, i, r) # 先对选择的数字进行一个随机化
key = A[r]
i = p - 1
for j in range(p, r):
if A[j]<=key:
i = i + 1
exchange(A, i, j)
exchange(A, i+1, r)
return i + 1
def quick_select(A, p = None, r = None, i = None):
if p == None:
p = 0
r = len(A)-1
i = int((p+r)/2)
if p == r:
return A[p]
q = partition(A, p, r)
if i == q:
return A[q]
elif i < q:
return quick_select(A, p, q-1, i)
else:
return quick_select(A, q+1, r, i)
它可能考虑的是采用绝对的下标会带来一些bug的问题? 不是很懂.
下面我们来分析一下这个算法的运行时间:
显然, 如果我们非常倒霉的话: 我们很有可能需要在确定了数组当中的n-1个元素之后再能最终确定我们要寻找的数字在什么地方, 当然啦, 这是一种非常极端的情况, 下面我们利用概率来分析一下相对一般的情况:
我们设该算法在一个含有n个元素的输入数组A[p…r]上的运行时间是一个随机变量, 记为
T
(
n
)
T(n)
T(n). 下面我们可以得到
E
[
T
(
n
)
]
\mathrm{E} [T(n)]
E[T(n)]的一个上界:
我们定义随机变量
X
k
X_k
Xk为:
X
k
=
I
{
子
数
组
A
[
p
.
.
q
]
正
好
包
含
k
个
元
素
}
X_k = I\{子数组A[p..q]正好包含k个元素\}
Xk=I{子数组A[p..q]正好包含k个元素}
然后, 假设元素是互异的, 我们有:
E
[
x
k
]
=
1
/
n
\mathrm{E}[x_k]=1/n
E[xk]=1/n
为了得到上界, 我们不妨假设每一次我们都需要调用较大的那个子数组, 那么我们不难得出:
T
(
n
)
≤
∑
k
=
1
n
X
k
⋅
(
T
(
max
(
k
−
1
,
n
−
k
)
)
+
O
(
n
)
)
=
∑
k
=
1
n
X
k
⋅
T
(
max
(
k
−
1
,
n
−
k
)
)
+
O
(
n
)
\begin{aligned} T(n)&\le \sum^n_{k=1} X_k \cdot (T(\max (k-1, n-k))+O(n)) \\&=\sum^n_{k=1}X_k\cdot T(\max(k-1,n-k))+O(n) \end{aligned}
T(n)≤k=1∑nXk⋅(T(max(k−1,n−k))+O(n))=k=1∑nXk⋅T(max(k−1,n−k))+O(n)
稍加思考, 我们就可以把
O
(
n
)
O(n)
O(n)从求和符号当中取出来.
下面我们对这个式子两边取期望. 经过一波操作我们可以得到如下这个式子:
E
[
T
(
n
)
]
≤
2
n
∑
k
=
⌊
n
/
2
⌋
n
−
1
E
[
T
(
k
)
]
+
O
(
n
)
\mathrm{E}[T(n)]\le {2\over n}\sum^{n-1}_{k=\lfloor n/2\rfloor}\mathrm{E}[T(k)]+O(n)
E[T(n)]≤n2k=⌊n/2⌋∑n−1E[T(k)]+O(n)
反正经过最后一些其他的奇奇怪怪的操作, 我们可以证明这个算法的期望时间为
O
(
n
)
O(n)
O(n).