目录
第二章 算法基础
知识点
循环不变式
本章中提到了一个名词 —— 循环不变式,类似于数学归纳法,主要用来证明算法的正确性。
总结一下循环不变式的定义:数据必须满足的一个或者多个约束。
在迭代过程中,如果数据始终满足约束,则称这个循环不变式为真,反之为假。
可以通过检查以下几个性质,来判断循环不变式是否为真:
- 初始化:循环的第一次迭代之前,数据满足约束。
- 保持:如果循环的某次迭代之前数据满足约束,那么在下次迭代之前仍然符合。
- 终止:在最后一次迭代后,数据仍然满足约束。
增长量级
如果一个算法的最坏情况运行时间具有比另一个算法更低的增长量级,那么我们通过认为前者比后者更有效。因为随着输入规模的增大,常量因子以及低阶项的影响越来越小。
递归,分治与归并排序分别是什么
- 递归是一种编程技巧。
- 分治是一种解决问题的套路/思维方式/模板。
- 归并排序一种是利用分治的思维方式,解决具体问题的方案。
练习题
2.1-3
考虑以下查找问题:
输入:n 个数的一个序列 A = a 1 , a 2 , . . . , a n A={a_1, a_2, ...,a_n} A=a1,a2,...,an和一个值 v v v。
输出:一个整数 i
- 如果 v 在 A 中,那么 i 满足 A[i] = v,即 v 在 A 中的下标。
- 如果 v 不在 A 中,那么 i 为 -1。
写出线性查找的代码,它扫描整个数组寻找v。使用循环不变式来证明算法的正确性。
先上代码:
int find(const std::vector<int> &A, int v) {
int i = -1;
for (int j = 0; j < A.size(); j++) {
if (A[j] == v) {
i = j;
break;
}
}
return i;
}
先来看循环过程中,数据 i 需要满足的约束:在每次迭代开始前,在已访问的数字中,如果存在 v ,则 i 为其下标,否则 i 为 -1。
- 初始时:尚未访问任何数字,且此时 i = -1。数据满足约束。
- 保持:在某次迭代开始之前,不妨设该次迭代要访问的数字为 A[j]。如果 A[j] 为 v,本次迭代会将 i 置为 j,否则 i 仍为 -1。显然在下次迭代之前,i 的值仍然满足约束。
- 终止:因为每次迭代后,数据均符合要求。所以在算法终止时,即最后一次循环迭代结束后,数据仍然满足约束。
2.1-4
两个 n 位二进制整数分别存放在数组 A,B 中,现将 A,B 按位相加,结果存在 C 中。
// 下标越小对应的二进制位越低
std::vector<int> add(const std::vector<int> &a, const std::vector<int> &b) {
vector<int> sum(a.size()+1);
for (int i = 0; i < a.size(); i++) {
sum[i] += a[i] + b[i];
sum[i+1] += (sum[i]>>1);
sum[i] &= 1;
}
return sum;
}
2.2-1
用 Θ \Theta Θ 记号表示函数 n 3 / 1000 − 100 n 2 − 100 n + 3 n^3/1000 - 100n^2 - 100n + 3 n3/1000−100n2−100n+3。
在本章中, Θ \Theta Θ记号表示最坏情况运行时间,仅关心最重要的项,因此上述可表示为 Θ \Theta Θ(n^3)$。
2.2-2
写出选择排序算法的代码
void sort(std::vector<int> &data) {
for (int i = 0, n = data.size(); i+1 < n; i++) {
int index = i;
for (int j = i+1; j < n; j++) {
if (data[index] > data[j]) {
index = j;
}
}
std::swap(data[index], data[i]);
}
}
分析选择排序算法的循环不变式
循环不变式可以认为是:在循环过程中,数据需要满足的一些约束。
在选择排序中,数据需满足约束:在外层循环,第 i 次迭代开始前,数组的前 i 个元素为整个数组中最小的 i 个数,且从小到大依次排列。
为何仅需对前 n-1 个元素进行交换,而不是所有元素?
当对前 n-1 个元素进行交换后,前 n-1 个元素必然是整个数组中最小的 n-1 个数,即第 n 个数为整个数组中的最大值,无需再进行交换了。
最好及最坏情况的运行时间
从代面层面分析,并无任何剪枝操作,无论输入数据如何排列,都需要进行 n-1 次交换,以及 ( n − 1 ) ∗ ( n − 2 ) 2 \frac{(n-1)*(n-2)}{2} 2(n−1)∗(n−2) 次比较。
所以,最好及最坏的运行时间均为 Θ ( n 2 ) \Theta(n^2) Θ(n2)
2.2-3
假定要查找的元素等可能 的为数组中的任意元素,分析其最坏及平均情况运行时间。
不妨假设数组的元素互不相同,当要查找第 i 个元素时,需要访问 i 个元素。因为查找每个元素的概率相同,所以平均需要检查的元素个数为 n 2 \frac{n}{2} 2n。
当要查找的值为最后一个元素时,为最坏情况,需要查找 n 个元素。
用 Θ \Theta Θ 表示为:
- 平均运行时间: Θ ( n ) \Theta(n) Θ(n)
- 最坏运行时间: Θ ( n ) \Theta(n) Θ(n)
TODO: 如果数组中有重复元素呢?没想明白要如何证明。
2-3.1
使用图 2-4 作为模型,说明归并排序在数组 A = < 3 , 41 , 52 , 26 , 38 , 57 , 9 , 49 > A = <3,41,52,26,38,57,9,49> A=<3,41,52,26,38,57,9,49>上的操作。
2-3.2
重写 Merge,使之不使用哨兵,而是一旦数组 L 或 R 的所有元素均被复制回就立即停止,然后将另一个数组的剩余部分复制回 A。
void Merge(vector<int> &nums, int p, int q, int r) {
// 初始化数组 L, R
vector<int> L, R;
for (int i = p; i <= q; i++) {
L.emplace_back(nums[i]);
}
for (int i = q+1; i <= r; i++) {
R.emplace_back(nums[i]);
}
// 归并
int i = 0, j = 0;
while(i < L.size() && j < R.size()) {
if (L[i] < R[j]) {
nums[p++] = L[i++];
} else {
nums[p++] = R[j++];
}
}
// 复制剩余部分
while(i < L.size()) {
nums[p++] = L[i++];
}
while(j < R.size()) {
nums[p++] = R[j++];
}
}
2.3-3
使用数学归纳法证明:当 n 正好是 2 的幂时,以下递归式的解为 T ( n ) = n ∗ l g n T(n) = n*lgn T(n)=n∗lgn。
T ( n ) = { 2 , 若 n = 2 2 T ( n / 2 ) + n , 若 n = 2 k , k > 1 T(n) = \left\{ \begin{array}{rl} 2,&若 n = 2 \\ 2T(n/2) + n,&若 n = 2^k, k > 1 \\ \end{array}\right. T(n)={2,2T(n/2)+n,若n=2若n=2k,k>1
- 当 n = 2 时,易得 T ( 2 ) = 2 ∗ l g 2 = 2 T(2) = 2*lg2 = 2 T(2)=2∗lg2=2,显然此时等式成立。
- 假设当
n
=
2
k
(
k
>
1
)
n = 2^k (k >1)
n=2k(k>1) 时,
T
(
n
)
=
n
∗
l
g
n
T(n) = n*lgn
T(n)=n∗lgn 成立,则对于
n
′
=
2
k
+
1
=
2
n
n' = 2^{k+1} = 2n
n′=2k+1=2n 有:
T ( n ′ ) = 2 ∗ T ( n ) + n ′ = n ′ l g n + n ′ = n ′ ∗ ( l g n + 1 ) = n ′ ∗ l g n ′ T(n') = 2*T(n) + n' = n'lgn + n' = n'*(lgn+1) = n'*lgn' T(n′)=2∗T(n)+n′=n′lgn+n′=n′∗(lgn+1)=n′∗lgn′。
显然此时等式也成立。
综上,当 n 正好是 2 的幂时,上述递归式的解为 T ( n ) = n ∗ l g n T(n) = n*lgn T(n)=n∗lgn
2.3-4
利用递归式分析递归版本的插入排序的最坏情况运行时间。
先上代码
void sort(vector<int> &arr, int pos) {
if (pos <= 0) {
return;
}
sort(arr, pos-1);
int i = pos-1, key = arr[pos];
while(i > 0 && arr[i] > key) {
arr[i+1] = arr[i];
i -= 1;
}
arr[i+1] = key;
}
int main() {
vector<int> arr{5,4,3,2,1};
sort(arr, arr.size()-1);
return 0;
}
在输入数据为降序时,上述代码的运行时间最坏。将第 p o s ( 0 < = p o s < n ) pos ( 0 <= pos < n) pos(0<=pos<n) 个元素插入到指定位置,需要移动 p o s pos pos 个元素。
假设一次移动的代码为 M M M,一次插入的代价为 I I I,则有:
T ( n ) = { 0 , n = 1 T ( n − 1 ) + M ∗ ( n − 1 ) + I , n > 1 T(n) = \left\{ \begin{array}{r} 0,n = 1 \\ T(n-1) + M*(n-1) + I,n > 1 \\ \end{array}\right. T(n)={0,n=1T(n−1)+M∗(n−1)+I,n>1
2.3-5
写出二分查找的代码;证明最坏情况运行时间为 Θ ( l g n ) \Theta(lgn) Θ(lgn)。
// arr 应为升序排序
bool find(const int *arr, int l, int r, int val) {
if (l > r) { return false; }
int mid = (l+r)>>1;
if (arr[mid] < val) {
return find(arr, l, mid-1, val);
}
if (val < arr[mid]) {
return find(arr, mid+1, r, val);
}
return true;
}
int main() {
int arr[] = {0,1,27,33,41,59};
cout << find(arr, 0, 5) << endl;
return 0;
}
当 v 不在 arr 中时,上述代码的运行时间达到最坏情况。上述实现中,每调用一次 find 函数,问题的规模就会缩小为 ⌊ n 2 ⌋ , n = r − l + 1 \lfloor\frac{n}{2}\rfloor,n = r-l+1 ⌊2n⌋,n=r−l+1 。最坏情况下, find 函数的调用次数约为 l g n + 2 lgn+2 lgn+2次。
除去递归调用外,find 函数仅会执行若干次左移,比较等操作,显然这些操作的运行时间为常熟,设为 C。所以上述实现的运行时间可表示为:
T ( n ) = C ∗ ( l g n + 2 ) = Θ ( l g n ) T(n) = C*(lgn + 2) = \Theta(lgn) T(n)=C∗(lgn+2)=Θ(lgn)
2.3-6
可以使用二分查找将插入排序的最坏情况运行时间改进到 \Theta(n*lgn) 吗?
不能。因为还需要移动数组内的元素。
2.3-7
给定集合 S 及 整数 x,判断 S 中是否存在两个其和恰为 x 的元素。
借助 unordered_set 的话,可以实现 O(n)。😝
但是利用 set 的有序性,编程更加简单。
bool find(const set<int> &s, int x) {
for (auto v : s) {
if (v > x/2) { return false; }
int r = x - v;
if (r == v) { return false; }
if (s.count(r)) { return true; }
}
return false;
}
思考题 2-1
a. 证明:插入排序最坏情况可以在 Θ ( n k ) \Theta(nk) Θ(nk)时间内排序每个长度为 k 个 n/k 个子表。
处理一个长度为 k 个子表,插入排序的最坏情况运行时间为 Θ ( k 2 ) \Theta(k^2) Θ(k2)。处理 n/k 个子表的最坏情况运行时间为 T ( n , k ) = n / k ∗ Θ ( k 2 ) = Θ ( n k ) T(n,k) = n/k * \Theta(k^2) = \Theta(nk) T(n,k)=n/k∗Θ(k2)=Θ(nk)。
b. 表明在最坏情况下如何在 Θ ( n l g ( n / k ) ) \Theta(nlg(n/k)) Θ(nlg(n/k))时间内合并这些子表。
合并 n/k 个表,其递归树的高度为 Θ ( lg ( n / k ) ) \Theta(\lg (n/k)) Θ(lg(n/k))。
每一层需要合并 n 个数字,所以每一层的运行时间为 Θ ( n ) \Theta(n) Θ(n)。
所以整体的合并时间为 Θ ( n ∗ lg ( n / k ) ) \Theta(n*\lg (n/k)) Θ(n∗lg(n/k))。
c. 假定修改后的算法的最坏情况运行时间为 Θ ( n k + n lg ( n / k ) ) \Theta(nk + n\lg (n/k)) Θ(nk+nlg(n/k)),要使修改后的算法与标准的归并排序具有相同的运行时间,作为 n 的一个函数,借助 Θ \Theta Θ记号,k 的最大值时什么?
其实我也不晓得正解是啥,那就临事不决就选B吧。。
既然 k 的最大值要用一个 n 的函数表示,无非就是求在满足运行时间相同时,k 和 n 的关系,又考虑到 k 要比 n 小,那 k 的选择可能为(从几个初等函数里面选)
- k = n k = \sqrt n k=n
- k = lg n k = \lg n k=lgn
代入方案二验证下:
T
(
n
)
=
n
∗
lg
n
+
n
∗
lg
(
n
/
lg
n
)
=
n
∗
lg
n
+
n
∗
lg
n
−
n
∗
lg
lg
n
=
2
n
lg
n
−
n
∗
lg
lg
n
=
Θ
(
n
lg
n
)
/
/
忽
略
低
阶
项
和
常
数
\begin{aligned} T(n) &= n*\lg n + n*\lg(n/\lg n) \\ &= n*\lg n + n*\lg n - n*\lg \lg n \\ &= 2n\lg n - n*\lg \lg n \\ &= \Theta(n\lg n) // 忽略低阶项和常数 \end{aligned}
T(n)=n∗lgn+n∗lg(n/lgn)=n∗lgn+n∗lgn−n∗lglgn=2nlgn−n∗lglgn=Θ(nlgn)//忽略低阶项和常数
为啥不代入方案一?我代入了发现不对
找到了作者的答案,如下:
d.在实践中,我们应该如何选择 k
应该根据具体的编程语言,运行机器的配置,输入数据的特点等维度分析插入排序和归并排序的系数,选择比较恰当的 k。总之,我觉得这个 k 应该是要根据具体的场景,经过反复试验得出来的。
思考题 2-2
冒泡排序时一种流行但低效的排序算法,它的作用是反复交换相邻的未按次序排列的元素。
BUBBLE_SORT(A)
1 for i = 1 to A.length-1
2 for j = A.length downto i+1
3 if A[j] < A[j-1]
4 exchange A[j] with A[j-1]
假设 A’ 表示 bubble_sort(A) 的输出。为了证明 bubble_sort 证明,我们必须证明它将终止并且有:
A ′ [ 1 ] ≤ A ′ [ 2 ] ≤ . . . ≤ A ′ [ n ] A'[1] \le A'[2] \le ... \le A'[n] A′[1]≤A′[2]≤...≤A′[n]
其中 n = A.length。为了证明 bubble_sort 确实完成了排序,我们还需要证明什么?
还需要证明 A’ 中的元素和 A 中的元素相同,即 A’ 是 A 的一个全排列。
为第 2~4 的 for 循环精确地说明一个循环不变式,并证明该循环不变式成立。
循环迭代式就是在迭代过程中,数据需要满足的一些约束。
每次迭代开始前,数据需满足两个约束:
- A [ j ] A[j] A[j] 是 A [ j . . n ] A[j .. n] A[j..n] 中的最小值
- A [ j . . n ] A[j .. n] A[j..n]是2-4行循环开始前的 A [ j . . n ] A[j .. n] A[j..n]的一个全排列。
证明过程如下:
- 初始化:初始时 j = n,此时 A [ j . . n ] A[j .. n] A[j..n] 中仅有一个元素,显然满足上述约束。
- 保持:
- A[j] 与 A[j-1] 比较,如果 A[j] < A[j-1] 则交换。这使得 A[j-1]在3-4行的语句执行结束后成为 A [ j − 1.. n ] A[j-1 .. n] A[j−1..n] 中的最小值。
- 因为该次迭代开始前, A [ j . . n ] A[j..n] A[j..n]满足第二个约束,且本次迭代有可能的操作仅为交换A[j-1]和A[j],所以在3-4行的语句执行结束后 A[j-1] 也满足第二个约束。
- 最后,j = j-1,使得在下次迭代前循环不变式仍然成立。
- 终止:当 j = i 时循环结束。此时,A[i] 为 A [ i . . n ] A[i .. n] A[i..n] 中的最小值,且 A [ i . . n ] A[i .. n] A[i..n]是2-4行循环开始前的 A [ i . . n ] A[i .. n] A[i..n]的一个全排列。
为第 1~4 的 for 循环精确地说明一个循环不变式,并证明该循环不变式成立。
循环迭代式就是在迭代过程中,数据需要满足的一些约束。
每次迭代开始前,数据需要满足如下约束:
-
A [ 1.. i − 1 ] A[1 .. i-1] A[1..i−1] 包含了 A [ 1.. n ] A[1 .. n] A[1..n] 中最小的 i-1 个数字,且已排序。
-
A [ 1.. n ] A[1 .. n] A[1..n] 是1-4行循环开始前的 A [ 1.. n ] A[1 .. n] A[1..n] 前的一个全排列。
-
初始化:初始时 i = 1, A [ 1.. i − 1 ] A[1 .. i-1] A[1..i−1] 为空, A [ i . . n ] A[i .. n] A[i..n] 即为 A [ 1.. n ] A[1 .. n] A[1..n],显然满足上述约束。
-
保持:
- 迭代开始前, A [ 1.. i − 1 ] A[1 .. i-1] A[1..i−1] 已经包含了 A 中最小的 i-1 个数字且有序,又因为2-4行循环结束后,A[i] 为 A [ i . . n ] A[i .. n] A[i..n] 中最小值,所以2-4行循环结束后,A[1 … i] 包含了 A 中最小的 i 个数字且有序。
- 迭代开始前, A [ 1.. n ] A[1 .. n] A[1..n] 是1-4行循环开始前的 A [ 1.. n ] A[1 .. n] A[1..n] 前的一个全排列。迭代过程中只会改变元素的位置而不会增删元素,所以每次迭代后仍然满足该约束。
- 最后,i = i+1,使得下次迭代开始前,数据仍然满足上述约束。
-
终止:当 i = n 时,循环结束。此时 A[1…n-1] 包含了 A 中最小的 n-1 个数字且已排序,A[n] 为 A 中最大的元素。因此整个数组 A 已经是有序的了。
冒泡排序的最坏情况运行时间。
设每次比较的代价为 C,每次交换的代价为 E,则
T
(
n
)
=
∑
i
=
1
n
−
1
(
(
n
−
i
)
∗
C
+
(
n
−
i
)
∗
E
)
=
∑
i
=
1
n
−
1
(
n
∗
(
C
+
E
)
)
−
∑
i
=
1
n
−
1
(
i
∗
(
C
+
E
)
)
=
(
n
∗
(
n
−
1
)
−
(
n
∗
(
n
−
1
)
2
)
)
∗
(
C
+
E
)
=
n
2
−
n
2
∗
(
C
+
E
)
=
Θ
(
n
2
)
\begin{aligned} T(n) &= \sum_{i=1}^{n-1}((n-i)*C + (n-i)*E) \\ &= \sum_{i=1}^{n-1}(n*(C+E)) - \sum_{i=1}^{n-1}(i*(C+E)) \\ &= (n*(n-1)-(\frac{n*(n-1)}{2}))*(C+E) \\ &= \frac{n^2-n}{2}*(C+E) \\ & = \Theta(n^2) \end{aligned}
T(n)=i=1∑n−1((n−i)∗C+(n−i)∗E)=i=1∑n−1(n∗(C+E))−i=1∑n−1(i∗(C+E))=(n∗(n−1)−(2n∗(n−1)))∗(C+E)=2n2−n∗(C+E)=Θ(n2)
思考题 2-3
1 y = 0
2 for i = n downto 0
3 y = a[i] + x*y;
借助 Θ \Theta Θ 记号,分析上述代码的运行时间。
上述代码需要迭代 n+1 次。每次迭代仅执行一次乘法,加法及赋值操作,设代价C。
T
(
n
)
=
(
n
+
1
)
∗
C
=
Θ
(
n
)
T(n) = (n+1)*C = \Theta(n)
T(n)=(n+1)∗C=Θ(n)
编写伪代码来实现朴素的多项式求值算法,该算法从头计算多项式的每个项。
for(int i = 0; i <= n; i++) {
int val = a[i];
for (int j = 1; j <= i; j++) {
val *= x;
}
y += val;
}
外层循环需要迭代 n+1 次,内层循环每次需要迭代 i 次,故运行时间为
T ( n ) = ∑ i = 0 n i = n ∗ ( n + 1 ) 2 = Θ ( n 2 ) \begin{aligned} T(n) &= \sum_{i=0}^{n}i \\ &= \frac{n*(n+1)}{2}\\ & = \Theta(n^2) \end{aligned} T(n)=i=0∑ni=2n∗(n+1)=Θ(n2)
证明终止时 y = ∑ k = 0 n a k x k y =\sum_{k=0}^{n}a_{k}x^k y=∑k=0nakxk
先给出循环不变式:2-3行for循环每次迭代开始前,y需满足如下约束:
y = ∑ k = 0 n − ( i + 1 ) a k + i + 1 x k y =\sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k y=∑k=0n−(i+1)ak+i+1xk
- 初始化:初始时 i = n, ∑ k = 0 n − ( i + 1 ) a k + i + 1 x k = 0 \sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k = 0 ∑k=0n−(i+1)ak+i+1xk=0,显然满足约束。
- 保持:对于每次迭代,y 更新为
y = a i + x ∗ ( ∑ k = 0 n − ( i + 1 ) a k + i + 1 x k ) = a i + x ∗ ( a i + 1 x 0 + a i + 2 x 1 + . . + a n x n − ( i + 1 ) ) = a i + a i + 1 x 1 + a i + 2 x 2 + . . + a n x n − i = ∑ k = 0 n − i a k + i x k \begin{aligned} y &= a_i + x*(\sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k) \\ &= a_i + x*(a_{i+1}x^0 + a_{i+2}x^1 + .. + a_nx^{n-(i+1)}) \\ &= a_i + a_{i+1}x^1 + a_{i+2}x^2 + .. + a_nx^{n-i} \\ &= \sum_{k=0}^{n-i}a_{k+i}x^{k} \end{aligned} y=ai+x∗(k=0∑n−(i+1)ak+i+1xk)=ai+x∗(ai+1x0+ai+2x1+..+anxn−(i+1))=ai+ai+1x1+ai+2x2+..+anxn−i=k=0∑n−iak+ixk
最后, i = i − 1 i = i-1 i=i−1,在下一次迭代开始前,仍满足 y = ∑ k = 0 n − ( i + 1 ) a k + i + 1 x k y =\sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k y=∑k=0n−(i+1)ak+i+1xk。 - 终止:当
i
=
−
1
i = -1
i=−1时,循环结束。此时有:
y = ∑ k = 0 n − ( i + 1 ) a k + i + 1 x k = ∑ k = 0 n a k x k \begin{aligned} y &=\sum_{k=0}^{n-(i+1)}a_{k+i+1}x^k \\ &= \sum_{k=0}^{n}a_{k}x^k \end{aligned} y=k=0∑n−(i+1)ak+i+1xk=k=0∑nakxk
思考题 2-4
列出数组 <2,3,8,6,1>的5个逆序对。
设下标从 1 开始,则五个逆序对为 (1,5),(2,5),(3,4),(3,5),(4,5)。
由集合{1,2,3…n}构成的什么数组具有最多的逆序对?它有多少逆序对?
当数组中元素降序排列时有最多的逆序对,一共有 n 2 − n 2 \frac{n^2-n}{2} 2n2−n个。
插入排序的运行时间与输入数组中逆序对的数量之间有什么关系?
先把插入排序的伪代码贴在这里
INSERTION-SORT(A)
1 for j = 2 to A.length
2 key = A[j]
3 i = j-1
4 while i > 0 and A[i] > key
5 A[i+1] = A[i]
6 i = i-1
7 A[i+1] = key
再说结论:
设排序前A中逆序对的数量为 I I I,那么插入排序过程中:
- 移动元素的次数,即第五,六行代码执行的次数为 I I I。
- 比较元素的次数,即第四行代码执行的次数为 I + n − 1 I+n-1 I+n−1。
- 其他代码,即第二,三,七行代码执行的次数为 n − 1 n-1 n−1。
综上,插入排序的运行时间可表示为 Θ ( I + n ) \Theta(I+n) Θ(I+n)。
证明如下:
设以 j j j 为 s e c o n d second second 的逆序对 ( f i r s t , s e c o n d ) (first, second) (first,second) 有 k j k_j kj 个。
在开始执行4-6行循环之前, A [ 1.. i ] A[1..i] A[1..i] 中的前 i − k j i-k_j i−kj 个数字必然小于 A j A_j Aj,后 k j k_j kj 个数字必然大于 A j A_j Aj。
所以在本次迭代中,第四行代码必然执行 k j + 1 k_j+1 kj+1次,第五,六行代码必然执行 k j k_j kj次。
综上:
- 第五,六行代码的执行次数: k 2 + k 3 + . . + k n = I k_2+k_3 + .. +k_n = I k2+k3+..+kn=I
- 第四行代码的执行次数: ( k 2 + 1 ) + ( k 3 + 1 ) + . . ( k n + 1 ) = I + n − 1 (k_2+1) + (k_3+1) + .. (k_n+1) = I + n -1 (k2+1)+(k3+1)+..(kn+1)=I+n−1
- 第二,三,七行代码的执行次数为 n − 1 n-1 n−1
所以,插入排序的运行时间可表示为 Θ ( I + n ) \Theta(I+n) Θ(I+n)
设计一个 Θ ( n lg n ) \Theta(n\lg n) Θ(nlgn)的统计逆序对数量的算法
已在 LeetCode 的 剑指 Offer 51. 数组中的逆序对 验证通过。
class Solution {
public:
int count(vector<int> &nums) {
if (nums.size() <= 1) {
return 0;
}
int mid = nums.size()/2;
vector<int> prefix(nums.begin(), nums.begin() + mid);
vector<int> suffix(nums.begin() + mid, nums.end());
int anw = count(prefix) + count(suffix);
int i = 0, j = 0, pos = 0;
while(i < prefix.size() || j < suffix.size()) {
if (i >= prefix.size()) {
nums[pos++] = suffix[j++];
} else if (j >= suffix.size()) {
anw += j;
nums[pos++] = prefix[i++];
} else {
if (prefix[i] <= suffix[j]) {
anw += j;
nums[pos++] = prefix[i++];
} else {
nums[pos++] = suffix[j++];
}
}
}
return anw;
}
int reversePairs(vector<int>& nums) {
return count(nums);
}
};