《算法导论(第3版)》读书笔记(一)算法基础

时间:2022-11-24 07:58:14


本篇内容主要涉及《算法导论》一书中的第二章知识,涉及的内容有插入排序归并排序

插入排序

对于插入排序有个很明显的显示生活例子来帮助我们理解,插入排序的工作原理就像打扑克牌一样,右手从桌面上拿起一张牌,然后再左手那一堆已经按牌面大小排好序的牌找到这张牌应该在的位置,然后插入进去。通俗点讲,就是从未排序的那一堆里面拿出第一个元素,然后插入到排好序的那一堆里面的正确位置,总的来说就是如下图所示

插入排序工作原理分析

整个排序过程应该是这样的:
初始的无序数组:[5, 4, 3, 2, 1]
插入过程:在排好序的数组a[i,j-1],将没有排好序的那一堆里拿出第一个元素,也就是a[j],然后再前面排好序的里面找到自己的位置插进去,形成排好序的数组a[i,j],一直重复这个过程,直至全部元素排完序
[4, 5, 3, 2, 1]
[3, 4, 5, 2, 1]
[2, 3, 4, 5, 1]
[1, 2, 3, 4, 5]

代码部分(非降序排列)

插入排序的伪代码

INSERTION-SORT(A)
for j = 2 to A.length
key = A[j]
i = j-1
while i>0 and A[i]>key
A[i+1] = A[i]
i = i-1
A[i+1] = key

插入排序C++实现

void InsertionSort(int a[],int n)
{
for(int j = 2;j<=n;j++)
{
int key = a[j]; //为待插入元素
int i = j-1;
while(i>0 && a[i]>key)
{
//去给key找到适合插入的位置,并且将找到插入位置之前的元素都往后移
a[i+1] = a[i];
i--;
}
a[i+1] = key;
}
}

插入排序python实现

def InsertionSort(a):
for j in range(1,len(a)):
key = a[j]
i = j-1
while i>=0 and a[i]>key:
a[i+1] = a[i]
i = i-1
a[i+1] = key

习题

2.1-1

[31,41,59,26,41,58]
[31,41,59,26,41,58]
[31,41,59,26,41,58]
[26,31,41,59,41,58]
[26,31,41,41,59,58]
[26,31,41,41,58,59]

2.1-2

//非升序伪代码实现
INSERTION-SORT(A)
for j = 2 to A.length
key = A[j]
i = j-1
while i>0 and A[i]<key //这里改一下比较符号就好了
A[i+1] = A[i]
i = i-1
A[i+1] = key

2.1-3

search-v(A)
for i = 1 to A.length
if v== A[i]
return i
return NIL

初始化:在第一次循环迭代之前,i=1时,子数组为A[1..i],如果A[1]==v就返回1,否则返回NIL
保持:每次都往子数组多增加一个元素,也就是多查找A[i+1],那么多做一次比较,因此开始那么回事
终止:无论怎么样,该算法一定有返回值,如果在还没有遍历完时已经找到了目标,那么就返回下标,如果还没找到就返回NIL

2.1-4

BINARY-ADD(A,B,C)
flag = 0 //判断是否进位
for i = 1 to A.length
key = A[i]+B[i]+flag //当前为是等于A[i]+B[i]+前面进的位
C[i] = key mod 2
if key>1 //如果当前位大于1了,需要进位
flag = 1
else //否则进位置零
flag = 0
if flag == 1
c[n+1] = 1

分析算法

1、RAM(random-access machine)模型分析通常能够很好地预测实际计算机上的性能,RAM计算模型中,指令一条接一条地执行,没有并发操作。RAM模型包含真实计算机中常见的指令:算法指令、数据移动指令和控制指令,总之,这本书的大部分章节都是用这种模型进行分析的
2、一个算法在特定输入上的运行时间是指执行的基本操作数或步数
3、这本书一般只讨论最坏情况运行时间的原因:

  • 一个算法的最坏运行时间是任何输入下运行时间的一个上界
  • 对于某些算法,最坏情况出现时相当频繁的
  • 大致上来看,“平均情况”通常与最坏情况一样差

习题

2.2-1 Θ(n^3)

2.2-2

SELECTION-SORT(A)
for i = 1 to A.length-1
key = i
for j = i+1 to A.length
if(A[j]<A[key])
key = j
swap(A[i],A[pos]) //交换元素

循环不变式:
初始化:i=1,从子数组A[1,n]里找到最小值A[pos],并与A[i]互换,此时只有A[1]这个元素是排好序的
保持:若A[1..i]是排好序的子数组,而A[i+1..n]的最小元素也必然大于A[i],先找出最小值与A[i+1]进行互换,这样就会得到A[1..i+1]称为有序的
终止:当i=n-1时终止,此时已经得到排好序的数组
最好时间复杂度和最坏时间复杂度一样:Θ(n^2)

2.2-3

最坏情况:找不到,也就是要遍历完全部数组,要找n次 ,Θ(n)
平均:n/2,因为每个元素是不是查找数的概率是一样的

2.2-4

我怎么知道,应该是用脑子修改吧

归并排序

上面的插入排序采用的是增量方法,而归并排序采用的是分治法。分治法,通俗点来讲就是大事化小,小事化了

分治法

分治法的思想:就像上面说的大事化小,小事化了,分治模式的主要三个步骤:
分解原问题为若干个子问题,这些子问题是原问题的规模较小的实例
解决这些子问题,递归的求解这些子问题,若子问题足够小,就直接求解
合并这些子问题的解成原问题的解
对于归并排序来说就是完全遵循上述分治的步骤的:将待排序的n个元素分解为个具n/2个元素的子序列,使用归并排序递归的排序这两个子序列,合并两个已经排好序的子序列

对于分解来排序的话,当待排序的序列长度为1时就已经排好序了,剩下的就是解决合并的过程了。其实对于合并的过程,很像,桌面上有两堆牌,都是排好序的,然后你要把这两堆牌拿到手上,使得手上拿着的牌也是有序的,那你是不是先开最上面的那张牌那张小,然后把那种小的牌拿到手上,以此类推,直到把全部牌拿完,所以对于合并操作,时间复杂度是线线的

整个归并排序的过程大致如下:


时间复杂度的话应该是Θ(nlgn),详细分析见书p20-21

代码部分

书上的伪代码

MERGE(A,p,q,r)
//对A[p,q]和A[q+1,r]进行合并
n1 = q-p+1
n2 = r-q
//声明一个新数组用来存A[p,q]和A[q+1,r]
let L[1..n1+1] and R[1..n2+1] be new arrays
for i = 1 to n1
L[i] = A[p+i-1]
for i = 1 to n2
R[i] = A[q+i]
L[n1+1] = ∞
R[n2+1] = ∞
i = 1
j = 1
for k = p to r
if L[i] to R[j] //比较第一张牌的大小,然后把它拿起来
A[k] = L[i]
i = i+1
else
A[k] = R[j]
j = j+1
MERGE-SORT(A,p,r)
if p < r
q = (p+r)/2
MERGE-SORT(A,p,q)
MERGE-SORT(A,q+1,r)
MERGE(A,p,q,r)

C++伪代码实现

void merge(int a[],int p,int q,int r)
{
int n1 = q-p+1,n2 = r-q;
int L[maxn],R[maxn];
for(int i=1;i<=n1;i++)
L[i] = a[p+i-1];
for(int i=1;i<=n2;i++)
R[i] = a[q+i];
R[n2+1] = L[n1+1] = inf;
int j = 1,i=1;
for(int k = p;k<=r;k++)
{
if(R[j]>L[i])
{
a[k] = L[i];
i++;
}
else
{
a[k] = R[j];
j++;
}
}
}
void mergeSort(int a[],int p,int r)
{
if(p<r)
{
int q = (p+r)/2;
mergeSort(a,p,q);
mergeSort(a,q+1,r);
merge(a,p,q,r);
}
}

python实现伪代码

def merge(a,p,q,r):
n1 = q-p+1
n2 = r-q
L = [0]
R = [0]
for i in range(1,n1+1):
L.append(a[p+i-1])
for i in range(1,n2+1):
R.append(a[q+i])
L.append(0x7fffffff)
R.append(0x7fffffff)
i,j = 1,1
for k in range(p,r+1):
if L[i]>R[j]:
a[k] = R[j]
j = j+1
else:
a[k] = L[i]
i = i+1
def mergeSort(a,p,r):
if p<r:
q = (p+r)/2
mergeSort(a,p,q)
mergeSort(a,q+1,r)
merge(a,p,q,r)

习题

2.3-1

[3] + [41] -> [3,41]
[52] + [26] -> [26,52]
[38] + [57] -> [38,57]
[9] + [49] -> [9,49]
[3,41] + [26,52] -> [3,26,41,52]
[38,57] + [9,49] -> [9,38,49,57]
[3,26,41,52] + [9,38,49,57] -> [3,9,26,38,41,49,52,57]

2.3-2

MERGE(A,p,q,r)
//对A[p,q]和A[q+1,r]进行合并
n1 = q-p+1
n2 = r-q
//声明一个新数组用来存A[p,q]和A[q+1,r]
let L[1..n1+1] and R[1..n2+1] be new arrays
for i = 1 to n1
L[i] = A[p+i-1]
for i = 1 to n2
R[i] = A[q+i]
i = 1
j = 1
for k = p to r
if i<=n1 and j<=n2
if L[i] to R[j]
A[k] = L[i]
i = i+1
else
A[k] = R[j]
j = j+1
else
if i>n1 and j<=n2
A[k] = R[j]
j = j+1
if j>n2 and i<=n1
A[k] = L[i]
i = i+1

2.3-3

几万年没写过数学归纳法了。。。

2.3-4

INSERTION(A,p,r)
for j = p to r
key = a[i]
i = j-1
while i>0 and A[i]>key
A[i+1] = A[i]
i = i-1
A[i+1] = key
INSERTION-SORT(A,p,r)
if p<r
r = r-1
INSERTION-SORT(A,p,r)
INSERTION(A,p,r)

2.3-5

BINARY(A,p,r,v)
for j = p to r
if A[j]==v
return k
return NIL
BINARY-SEARCH(A,p,r,v)
if p<r
q = (p+r)/2
if A[q]>v
BINARY-SEARCH(A,p,q,v)
return BINARY(A,p,q,v)
else
BINARY-SEARCH(A,q+1,r,v)
return BINARY(A,q+1,r,v)
return NIL

2.3-6

加入二分查找虽然可以缩短找到正确位置的时间,但是你找到该位置插入这个元素的时候,会比之前花费更多的时间,所以最终的最坏时间复杂度还是n^2

2.3-7

先对集合S进行排序,然后遍历这个集合里的每个元素a[i],然后去找x-a[i]是否存在于这个集合里面,查找使用二分查找,所以总的时间复杂度就是nlgn

SLOVE(S,x)
for j = 1 to A.length
key = x-S[j]
pos = BINARY-SEARCH(S,1,A.length,key) //二分查找x-S[j]
if pos!= NIL
return true
return false

思考题

2-1

a. k^2 * n/k = nk
b. 每一次合并的时间是O(n),总共要合并lg(n/k)+1次,所以最坏时间复杂度应该是O(nlg(n/k))
c. 最大值是lgn
d. 当插入排序比归并排序快的时候

2-2

最坏情况与插入排序相同

2-3

a. O(n)
b. O(n*k)

2-4

a. (2,1) (3,1) (8,6) (8,1) (6,1)
b. 降序数组最多逆序对,总共有n*(n-1)/2
c. 逆序对越多运行情况越坏,因为这样需要插入的元素越多
d. 改一下归并排序的merge函数,如果L[i]>R[j],那么ans+=q-i+1