0%

说明

本文整合自王道和里昂学长的PPT(无盈利侵权必删),是完整的408代码大题解题方案,整合好的pdf文档可以联系我获取

快速排序

代码实现

王道强化课快速排序代码

快速排序算法以huafen()函数为核心

如何划分?

第一趟

首先将左指针L对应的数组元素设为mid值,我们之后要将mid值和其他值对比然后找到mid值该在的位置

先从R向前寻找第一个小于mid的值(如果没有R- -一直找),如果有,就把他和目前存着mid值的A[L]互换位置

再从L向后寻找第一个大于mid的值(如果没有L++一直找),如果有,就把上一步的A[R]换过来

第二趟

上一趟完成了第一次交换,把小于mid的换到了右边,而把大于mid的换到了左边

第二趟又在新的R找到了小于mid的元素,放在之前发现的左边覆盖,以此类推

最后一趟

当最后一趟开始时,若是L++导致L==R则

A[L]换上了右边更小一点的元素。

而下一步L++,已不满足L<R的条件。而这一步的L就是上一步的R(L==R),A[R]的值已经迁移到了其左边。

因此设A[L]=mid,就把两者很好的拆开了

当最后一趟开始时,若是R- -导致L==R则

也就是上一趟将**大于mid的A[L]**的值放到右边时,将较大的值换到了A[R]中。

此时R- -到R==L,因此此时A[L]中的存储的是已经是到了还没有**R–**时的右边。

因此A[L]=mid,即可拆开

快速排序算法

因此在快排中调用这段代码

先将A中的LR成以mid为中间的两段,左边小于mid,右边大于mid

我们的划分传来的是mid的位置,因此要想把整个数组排序好,只需要排好左右一直调用。

将数组的最左边L和mid的前一个位置进行划分,穷举下去即可解决

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void Qsort(int A[], int L, int R) {
if(L > R) return; //递归终止
int M = huafen(A, L, R);
Qsort(A, L, M-1); //左半部分快排
Qsort(A, M+1, R); //右半部分快排
}

int hualen(int A[], int L, int R) {
int mid = A[L];
while(L < R) {
while(A[R] >= mid && L < R) R--;//等于号能保证其相对位置不变,稳定性这一块
A[L] = A[R];
while(A[L] <= mid && L < R) L++;
A[R] = A[L];
}
A[L] = mid;
return L;
}

划分函数的妙用

求数组中第k小元素的方法

王道强化课huafen函数

按理说我们只需要快速排序好,再直接利用数组的随机查找特性找到A[k-1]就可以

但是huafen函数可以直接找到某个元素的具体位置,我们可以直接用M接收huafen函数传来的此时的位置

若这个位置比已知位置小,则k-1在M前面,则设L=M+1继续试

1
2
3
4
5
6
7
8
9
10
int func(int A[], int n, int k) {
int L = 0, R = n - 1, M = 0;
while(1) {
M = huafor(A, L, R);
if(M == k - 1) break;
else if(M > k - 1) R = M - 1;
else if(M < k - 1) L = M + 1;
}
return A[k-1];
}

归并排序

归并排序用于两个数组排到一个数组里的排序中

归并排序王道强化课

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int Merge(int A[], int N, int B[], int M, int C[]) {
int i = 0, j = 0, k = 0;
while (i < N && j < M) {
if (A[i] <= B[j])
C[k++] = A[i++];
else
C[k++] = B[j++];
}
while (i < N)
C[k++] = A[i++];
while (j < M)
C[k++] = B[j++];
return 1;
}

链表

查找+删除

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//删除值为 x 的结点
void deletX(LinkList L, int x) {
LNode *pre = L; //pre指向p的前驱结点
LNode *p = pre->next; //p指向下一个要处理的结点
while (p != NULL) {
//对当前结点p进行处理
if (p->data == x) {
LNode *q = p; //删除并释放值为x的结点
p = p->next; //p指向后一个结点
pre->next = p; //修改前驱结点的next指针
free(q);
} else {
pre = p; // pre、p 后移
p = p->next;
}
}
}

查找+插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//在递增有序链表中插入值为 x 的信息
void InsertX(LinkList L, int x) {
LNode *pre = L; //pre指向p的前驱结点
LNode *p = pre->next; //p指向下一个要处理的结点
while (p != NULL) {
//对当前结点p进行处理
if (p->data > x) {
break; //应插入到p的前面
} else {
pre = p; // pre, p 后移
p = p->next;
}
}
LNode *q = (LNode *)malloc(sizeof(LNode));
q->data = x;
q->next = p;
pre->next = q;
}

头插法(原地逆转链表)

在勾画的绿色部分正是逆置的核心出装,将新插入的结点(在原链表中更靠后)不断放前,而实现逆置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void ListReserve(LinkList L){
//分配一个辅助头结点
LinkList head = (LNode *) malloc(sizeof(LNode));
head->next = NULL;

while (L->next != NULL){
LNode *p = L->next;
L->next = L->next->next;
p->next = head->next;
head->next = p;
}
L->next = head->next;
free(head); //释放辅助头结点
}

尾插法保持原序

以下是我自己写的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void ListPart(LinkList C){
LinkList A=(LNode*)malloc(sizeof(LNode))//A和B的头结点来也
LinkList B=(LNode*)malloc(sizeof(LNode))
A->next=NULL;
B->next=NULL;
int count=1;//计数器:判断奇偶结点
LNode *q=A;//A的尾指针
while(C->next != NULL){
if(count%2 == 1)//若为奇数,尾插法
{
LNode*p=C->next;//记录目前要拆掉的结点
C->next=C->next->next;//断开该结点
q->next=p;//插入尾结点之后
p->next=NULL;//断掉和原表最后的联系
q=p;//更新尾指针
count++;//更新计数值
}
if(count%2 == 0)//若为偶数,头插法
{
LNode *p = C->next;//记录目前要拆掉的结点
L->next = C->next->next;//断开该结点
p->next = B->next;//将最新的结点插到最头边,保证逆序
B->next = p;//作为头结点的下一个结点
count++;//更新计数值
}
}
free(C);//正式消灭C表
}

二叉树

前/中/后序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//二叉树的结点定义(链式存储)
typedef struct BiTNode{
int data; //数据域
struct BiTNode *lchild,*rchild; //左、右孩子指针
}BiTNode,*BiTree;

//先序遍历二叉树
void PreOrder (BiTree root){
if (root==NULL)
return;
visit(root); //访问根节点
PreOrder(root->lchild);
PreOrder(root->rchild);
}

//中序遍历二叉树
void InOrder (BiTree root){
if (root==NULL)
return;
InOrder(root->lchild);
visit(root); //访问根节点
InOrder(root->rchild);
}

//后序遍历二叉树
void PostOrder (BiTree root){
if (root==NULL)
return;
PostOrder(root->lchild);
PostOrder(root->rchild);
visit(root); //访问根节点
}

层序遍历

在层序遍历中需要使用到队列,也就需要队列的操作

1
2
3
4
5
6
7
8
//初始化队列
void InitQueue(Queue &Q)
//判断队列是否为空
bool IsEmpty(Queue Q)
//新元素x入队
void EnQueue(Queue &Q, BiTNode * x)
//队头元素出队,并使用x 返回队头元素
void DeQueue(Queue &Q, BiTNode * &x)//&x 表示引用参数,函数内部对x的修改会影响外部的x
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//层序遍历
void LevelOrder(BiTree T){
Queue Q;
InitQueue(Q); //初始化辅助队列
BiTree p;
EnQueue(Q, T); //将根结点入队
while(!IsEmpty(Q)){
DeQueue(Q, p);
visit(p);
if(p->lchild != NULL)
EnQueue(Q, p->lchild); //左孩子入队
if(p->rchild != NULL)
EnQueue(Q, p->rchild); //右孩子入队
}
}

求树高

(方法一)先序遍历

1
2
3
4
5
6
7
8
9
10
/*求树的高度(方法一)*/
int height = 0; //用全局变量记录树的高度
void PreOrder(BiTree T, int n) {
if (T == NULL)
return;
if (n > height)
height = n; //更新树的高度
PreOrder(T->lchild, n + 1); //遍历左子树
PreOrder(T->rchild, n + 1); //遍历右子树
}

调用时用**PreOrder(T, 1**)

原理是每过一层则+1并和目前高度比较

(方法二)后序遍历

1
2
3
4
5
6
7
8
9
10
int PostOrder(BiTree T){
if (T == NULL)
return 0;
int left = PostOrder(T->lchild);
int right = PostOrder(T->rchild);
if (left > right)
return left + 1;
else
return right + 1;
}

以该图为例,g和h没有左右结点了,故直接返回0+1给e作为e的left和right,e再进行比较以大的深度+1作为自己的深度交给b,b也这么做并和c一起交给a

后序遍历以孩子出发从孩子开始数并返给根节点最长的版本

求树宽

此处先用先序遍历统计每一层的结点数,也就是使用先序遍历先根后孩子的特点来统计

1
2
3
4
5
6
7
8
9
int width[MAX];  //用于统计各层的结点总数

//先序遍历,同时统计各层结点总数
void PreOrder(BiTree T, int level) {
if (T == NULL) return;
width[level]++; //累加该层结点总数
PreOrder(T->lchild, level + 1); //遍历左子树
PreOrder(T->rchild, level + 1); //遍历右子树
}

再找到宽度数组中最大的一项,将其作为树宽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void treeWidth(BiTree T) {
// 初始化宽度数组
for (int i = 0; i < MAX; i++)
width[i] = 0;
PreOrder(T, 0); // 从第0层开始遍历

// 找出最大宽度(某一层最多的节点数)
int maxWidth = 0;
for (int i = 0; i < MAX; i++) {
if (width[i] > maxWidth)
maxWidth = width[i];
}
printf("树的宽度是%d", maxWidth);
}

求树WPL

求树的WPL还是要使用树的先序遍历

使用了求树高和树宽的思想

先找到叶子节点(也就是左右孩子为空)并且将此处的带权路径长度加入WPL

如果不是叶子结点则向下找到其他叶子节点高度

1
2
3
4
5
6
7
void PreOrder (BiTree T, int level){
if (T == NULL) return;
if(T->lchild==NULL && T->rchild==NULL)
WPL += level*T->weight; //累加叶结点带权路径长度
PreOrder(T->lchild, level + 1); //遍历左子树
PreOrder(T->rchild, level + 1);
}

特殊树型判断

二叉排序树判定

中序遍历出来的序列是升序序列,所以二叉排序树序列中下一个结点必然大于前面所有的结点(也就是MAX{前面结点})这里用temp表示

所以只要后面遍历到的小于temp,则直接判定失败

若不小于则继续向右子树遍历,直到为NULL退回

1
2
3
4
5
6
7
8
9
10
int temp=MIN_INT; //记录当前遍历到的最小值
bool isBST=true; //是否为二叉排序树?

void InOrder(BiTree T){
if (T == NULL) return;
InOrder(T->lchild);
if (T->data >= temp) temp=T->data;
else isBST=false;
InOrder(T->rchild);
}

平衡二叉树判定

此树判定和方法二找树深一样

通过后序遍历的特点:先访问孩子再进行操作,由最后的孩子也就是结点A的左右子树都退回来自己的高度之后,再进行左右子树高度判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool isBalance=true;    //二叉树是否平衡
int PostOrder(BiTree T){
if (T == NULL)
return 0;
int left = PostOder(T->lchild);
int right = PostOder(T->rchild);

if (left-right>1) isBalance=false;
if (left-right<-1) isBalance=false;

//树的深度=Max(左子树深度,右子树深度)+1
if (left>right) return left+1;
else return right+1;
}

完全二叉树判定

这里先借用一下层序遍历的图

层序遍历,因为完全二叉树是按顺序排放,所以要看每一层的结点的逐个讨论、

四种情况分别在访问这个结点时讨论一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 特殊树形判定:完全二叉树?

bool isComplete = true; // 是否为完全二叉树
bool flag = false; // flag=true,表示层序遍历时出现过叶子或只有左孩子的分支节点

void visit(BiTNode * p) {
if (p->lchild == NULL && p->rchild == NULL) {
// 当前节点为叶子节点
if (flag) {
isComplete = false; // 不是完全二叉树
}
flag = true;
} else if (p->lchild != NULL && p->rchild == NULL) {
// 只有左孩子,没有右孩子
if (flag) {
isComplete = false; // 不是完全二叉树
}
flag = true;
} else if (p->lchild == NULL && p->rchild != NULL) {
// 只有右孩子,没有左孩子 - 肯定不是完全二叉树
isComplete = false;
} else {
// 有两个孩子
if (flag) {
isComplete = false; // 不是完全二叉树
}
}
}

图的数据结构定义

邻接矩阵

这是邻接矩阵的数据结构

这是邻接矩阵的初始化操作

邻接表

这是邻接表的数据结构,在图ALGraph中存储一个顶点表数组和顶点数和边数

在顶点表中存储这个结点的数据和第一个依附该点的边

边表则是指出这条边指向的点和下一条边的指针以及该边权值

这个表依旧要执行初始化操作

先将顶点表数组中的第一条边全部置为NULL

再将其顶点数和边数先更新

用封装好的AddEdge(图,出点,入点,权值)函数添加边

AddEdge函数是这么写的

首先先创建一个边结点,将其的权值和指向的结点j配置好

再将他连接到i结点第一条边之前,让这条新边指向老的第一条边(类似头插法将靠后的插在头边)

最后让i的点表连接到这条边作为first

最后可以看到他的逻辑结构长这样

图的遍历

深度优先遍历

里昂学长的pdf

1
2
3
4
5
6
7
8
9
10
//邻接矩阵实现深度优先搜索
void DFS(MGraph G,int i){
visit(i);
//访问初始顶点
visited[i]=true;//对做已访问标记
for(int j=0;j<G.G.numVertices;j++)
if(visited[j]==false&&G.edge[i][j]==1){
DFS(G,j);//为的尚未访问的邻接点,递归访问
}
}

首先先把第一个访问的结点打上true,对第一结点比如A,找他向别人发射过的边,直到j<G.vexnum(穷尽了所有点)

当遇到没有遍历到过的且存在的邻接点时,将其投入DPS

然后一直DPS递归下去,直到再没有邻接的点,逐层返回没有访问完邻接点的结点中继续访问他们邻接点

1
2
3
4
5
6
7
8
9
10
11
12
13
//邻接表实现深度优先搜索
void DFS(ALGraph G,int i){
visit(i);
//访问初始顶点
visited[i]=true;//对做已访问标记
for(EdgeNode* p=G.VertexList[i].first ; p!=NULL; p=p->next){
//检测的所有邻接点
int j=p->index;
if(visited[j]==false){
DFS(G,j);//为的尚未访问的邻接点,递归访问
}
}
}

前面步骤一样,依旧从i点开始遍历。先找到i点的第一个边,将其位置存放到p中

j就是此时p指向的点,若此点没有被访问过则开始深度遍历

直到遍历到最后一个能遍历到的点,就往上返回,继续访问之前没有访问完其他边的结点。

广度优先遍历

里昂学长的pdf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//邻接矩阵实现广度优先搜索
void BFS(MGraph G, int v){
visit(v); //访问初始顶点
visited[v]=true; //对v做已访问标记
EnQueue(Q, v); //顶点v入队
while(!isEmpty(Q)){
DeQueue(Q, v); //从队首取出顶点v
for(int w=0; w<G.numVertices; w++){
//检测v的所有邻接顶点
if(visited[w]==false && G.edge[v][w]==1){
visit(w); //w为v的尚未访问的邻接点,访问w
visited[w]=true; //标记w为已访问
EnQueue(Q, w); //顶点w入队
}
}
}
}

广度优先遍历类似于图的层序遍历

从v出发,先将v存入队列中

当队列不为空时就出队一个结点,并且从第0个结点开始遍历,寻找v指向的且没有被访问过的结点,找到一个入队一个直到找不到为止

然后从i结点找到的第一个结点开始继续重复这个步骤,直到所有结点都找完使队列为空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//邻接表实现广度优先搜索
void BFS(ALGraph G, int v){
visit(v); //访问初始顶点
visited[v]=true; //对v做已访问标记
EnQueue(Q, v); //顶点v入队
while(!isEmpty(Q)){
DeQueue(Q, v); //从队首取出顶点v
for(EdgeNode *p=G.VertexList[v].first; p!=NULL; p=p->next){
//检测v的所有邻接顶点
int w = p->index;
if(visited[w]==false){
visit(w); //w为v的尚未访问的邻接点,访问w
visited[w]=true; //标记w为已访问
EnQueue(Q, w); //顶点w入队
}
}
}
}

套路和上面一样,入队之后先找到第一个边指向的结点看是否被访问过,若没有则全部入队直到v结点没有邻接的点为止。

然后又从v第一条边邻接点继续广度优先遍历

拓扑排序

先按一下思路分析

拓扑排序的步骤:

  1. 选择入度为0的顶点并输出
  2. 从图中删除该顶点与所有以它为弧的边
  3. 重复步骤1、2,直到不存在入度为0的顶点
拓扑序列 条件
存在且唯一 每次选择结点都只有一个结点可选
存在不唯一 选择结点时不止一个结点可选
不存在 图中有环,环中顶点未被访问

先拿王道的流程图说一下思想

首先我们需要循环找到入度为0的点并且全部入队。

此处如果队列里元素个数超过1,

  • 入度为0的点肯定超过2个也就是拓扑排序不唯一。
  • 若是有多个连通分量,一个连通分量多个入度为0,另一个连通分量是有环图,则很明显不存在拓扑排序

然后当我们出队时,我们将计数器+1,来记录已经出队的数

出队之后将所有出队的点指向的点,把他们的入度-1,然后继续寻找入度为0的点循环这个过程

当队空之后我们根据count数进行判断

  • 如果count等于顶点数,那么说明从顶点到最后一个点只有一条路径。没错,拓扑排序唯一
  • 若count数不等于也就是小于顶点数,说明存在环路,始终有点入度不为0互相堵着
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//邻接表实现
int uniquely1(MGraph G){
int inDegree[MAXV] = {0}; // 存储每个顶点的入度
int queue[MAXV]; // 用于存储入度为0的顶点
int front = 0, rear = 0; // 队列的前后指针
int count = 0; // 拓扑序列中的顶点计数

// 计算每个顶点的入度
for (int i = 0; i < G.vexnum; i++)
{
ArcNode *p = G.AdjList[i].firstarc;//从i的第一条边开始找
while (p != NULL)
{
inDegree[p->adjvex]++;
p = p->nextarc;
}
}

// 将所有入度为0的顶点入队
for (int i = 0; i < G.vexnum; i++)
{
if (inDegree[i] == 0)
queue[rear++] = i;
}

// 进行拓扑排序
while (front < rear)//非空队列
{
// 如果队列中有多个顶点,说明图的拓扑序列不唯一
if (rear - front > 1)
return 0;
int v = queue[front++]; // 取出队首顶点
count++;
// 更新该顶点的所有邻接顶点的入度
ArcNode *p = G.AdjList[v].firstarc;
while (p != NULL)
{
inDegree[p->adjvex]--;
// 如果入度变为0,则加入队列
if (inDegree[p->adjvex] == 0)
queue[rear++] = p->adjvex;
p = p->nextarc;
}
}

if (count == G.vexnum)
return 1; // 当前拓扑序列包含所有顶点,则图的拓扑序列唯一
else
return 0; // 当前拓扑序列不包含所有顶点,则图的拓扑序列不存在
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//邻接矩阵实现
int uniquely1(MGraph G)
{
int inDegree[MAXV] = {0}; // 存储每个顶点的入度
int queue[MAXV]; // 用于存储入度为0的顶点
int front = 0, rear = 0; // 队列的前后指针
int count = 0; // 拓扑序列中的顶点计数

// 计算每个顶点的入度
for (int i = 0; i < G.numVertices; i++)
for (int j = 0; j < G.numVertices; j++)
if (G.Edge[i][j] != 0)
inDegree[j]++;

// 将所有入度为0的顶点入队
for (int i = 0; i < G.numVertices; i++)
if (inDegree[i] == 0)
queue[rear++] = i;


// 进行拓扑排序
while (front < rear)
{
// 如果队列中有多个顶点,说明图的拓扑序列不唯一
if (rear - front > 1)
return 0;
int v = queue[front++]; // 取出队首顶点
count++;
// 更新该顶点的所有邻接顶点的入度
for (int i = 0; i < G.numVertices; i++)
if (G.Edge[v][i] != 0)
{
inDegree[i]--; // 如果入度变为0,则加入队列
if (inDegree[i] == 0)
queue[rear++] = i;
}
}

if (count == G.numVertices)
return 1; // 当前拓扑序列包含所有顶点,则图的拓扑序列唯一
else
return 0; // 当前拓扑序列不包含所有顶点,则图的拓扑序列不存在
}

DPS的实现

首先DPS的实现较为容易,这里给出伪代码和思想

借鉴自图——深度优先遍历(DFS)实现有向无环图的逆拓扑排序_dfs实现逆拓扑排序-CSDN博客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//返回有向图G中顶点v的第一个邻接点,如没有返回-1
int FirstNeighbor(Graph G, int v)
{
//(......具体实现细节)
return w;
}

//假设图G中w是v的一个邻接点,返回除w外的v的下一个邻接点,若没有,返回-1
int NextNeighbor(Graph G, int v, int w)
{
//(......具体实现细节)
return w;
}

//访问v顶点
void visit(int v);

//标记顶点被访问状态的数组,初始时全为false
bool visited[MAX_NUM];

//以上是DPS代码需要的函数
void DFS(Graph G, int v)
{
visit(v);
visited[v] = true; //顶点v已被访问,也就是入度为0的点
//这里开始循环遍历,先从G图的v点的邻接表的第一根弧开始,若该点尚未访问则访问并递归该点,寻找该点的边表,以此寻找到其邻接结点
//在其邻接表的边表后没有边后,递归结束并退回上一个递归,上一个递归便可以继续寻找他的其他邻接节点
for (w = FirstNeighbor(G, v); w != -1; w = NextNeighbor(G, v, w))
{
if (visited[w] == false)
DFS(G, w);
}

这就是DPS的内核,找到源点第一个邻接节点,然后通过递归寻找这个节点的邻接节点,当穷尽最深后,便会退回上一个节点,寻找该节点的第二个邻接节点

DPS实现拓扑排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//为保证所有顶点都被排序,需遍历所有顶点
for (int i = 0; i < MAX_NUM; i++){
if (!visited[i])
DFS(G, i);
}

void DFS(Graph G, int v){
visited[v] = true;
for (w = FirstNeighbor(G, v); w != -1; w = NextNeighbor(G, v, w)){
if (!visited[w])
DFS(G, w);
}
print(v); //输出v信息进行逆拓扑排序
}

这里就是内核所在,在DPS退出之后才输出,因此祖宗结点反而要在最后输出,因为他只有穷尽所有邻接节点的邻接节点后,**DPS(祖宗)**才会在栈中被放出来

以该图为例,a检查到邻接结点b,b未被访问过

于是调用DPS(B),B也找自己的邻接节点,故只有当调用DPS(d)返回并输出d,才能解放c乃至最下面的a

王道书上没有解释,我自己理解如下:

先给出一个无向图的邻接矩阵,我们先探讨中的元素的意义,是A中的第二行×第三列。我们可以知道当某两项对应元素相乘时不为零的情况下是都为1,也就是点2到某点有边,且某点到3也有边

因此我们可以得出结论,这一行×一列的结果便是2到3路径为2的路径数

推广到一般结论便是:

设图 G 的邻接矩阵为 A,的元素等于由顶点i到顶点j的长度为 n 的路径的数目

准备食材

需要准备的有:

  1. 老酸奶两盒约250g

  2. 淡奶油一盒约250g

  3. 吉利丁片

  4. 抹茶粉

  5. 白砂糖(木糖醇)

  6. 饼干85g(我这里选的好吃点蔬菜饼干,根据自己喜好选择)

  7. 黄油30g

蛋糕底部制作

将我们买的饼干放入袋子或者裱花袋,用擀面杖碾成颗粒状

之后我们拿出提前用热水泡化的液体黄油和饼干混合

这步是为了饼干不松散能成为蛋糕的基底

最后我们把搅拌好的饼干底搅拌均匀,使其都有粘合在一起的状态

放入我们的模具中铺平

这样我们这部就算完成了

慕斯糊糊制作

先将老酸奶倒入两罐,并且搅拌至酸奶状(把酸奶搅拌至酸奶状这块),也就是搅拌顺滑

然后我们把冷水中泡软的吉利丁片再倒入热热的牛奶里,并搅拌至完全融合

再将我们的牛奶不明体倒入老酸奶中,将他们搅拌混合

再将我们冷藏了24h(其实没有这么久,因为刚从超市保鲜柜买回来)的淡奶油打发至蓬松状

将其倒入我们的糊糊,现在糊糊已经完整了,分成两份,一份倒入抹茶粉一份不倒入,这样倒在一起会形成颜色层次

最后把两份慕斯糊糊分别倒入模具,送入冰箱冷藏4h

END成品图

首先,两个结论:

前不能前:前序线索二叉树没有有效算法能够通过指针找到前驱

后不能后:后序线索二叉树没有有效算法能够通过指针找到后继

然后讨论中序线索二叉树找后继

1
2
3
4
ThreadNode *Nextnode (ThreadNode *p) {
if(p->rtag==0) return Firstnode(p->rchild); //右子树中最左下结点
else return p->rchild; // rtag==1 则直接返回后继线索
}

若A有右孩子,这个函数将会找到A的右孩子并一直往左走,停下的瞬间就是他的后继节点

若没有便会直接访问他的后继指针

寻找前驱也很简单,只需要寻找左孩子的最后一个右super孙子结点,无需赘述。也就是中序线索二叉树可以完美找到自己的前驱后继

我们再以B为当前节点讨论,若是先序线索二叉树,B(因为有左孩子)则无法找到他的大爹结点,也就是前驱A

若是后序线索二叉树,B(因为有右孩子)则无法找到他的亲爹指点,也就是后继A

因此前不能前后不能后,中都中!

  • 高等数学积分公式大全

    一、基本积分公式

    1. 幂函数积分


    说明:n=-1时为特殊情况,转化为对数函数积分

    2. 指数函数积分


    二、三角函数积分

    1. 基本三角函数


    2. 衍生三角函数




    3. 正割余割平方积分

    4. 正割余割乘积积分

    三、反三角函数积分

    1. 反正切类


    2. 反正弦类


    四、其他重要积分

    1. 双曲函数类


    2. 分式积分

    五、积分技巧公式

    1. 平方三角函数


    2. 其他实用公式

next数组的用法

首先,当我们进行KMP匹配的时候,我们会用主串和模式串进行对比。当失配的时候我们需要把 指向模式串的指针指向next[j]。

原因是因为在主串指针前的所有内容是可以用共同的前后缀来进行匹配的,next数组也是用共同的前后缀来得出的

next数组的求法

将指向模式串的指针Pj,前面的部分比如目前指向了j=5的位置。我们可以看到1234中最大的前后缀匹配就是{ab}也就是说,我们直接指向前缀ab之后的元素即可,也就是令模式串的指针的j=3

由此我们得到的next数组就可以用于KMP算法模式串指针的移动

nextval的求法

但是如果我们按照next数组可能会出现,指针在5失配,指针又转向3同时又失配,不得不转向1结果又失配,因为他们失配的原因是跳转到的字母和目前失配的字母一样,所以会连续失配

因此我们只需要从头到后检查,自己要跳转的字母是否和自己一致,若和自己一致,则追根溯源到最开始这个字母的失配next[j]

  • 原函数存在原理中写道,在区间内有第一类间断点则没有原函数
  • 而可积的条件只需要有界且区间内只有有限个间断点。
  • 而变上限积分的连续性中说可积,则就连续。
  • 也就是说存在第一类间断点的条件下可以存在连续的变上限积分,而不能存在原函数
  • 我觉得这就是两者最大的区别,原函数本质是求导的逆运算,而变上限积分是面积,他的可导性则是的值的体现。
  • 因为可去间断点左右极限相等,所以可导,导数是在此处的极限值
  • 因为跳跃间断点左右极限相等,所以连续但不可导,左右导数是在此处左右的极限值
  • 只有当连续时,才是他的原函数!因为存在一类间断点时,并不等于函数值,自然不是原函数

人不能老是活在抽象中,地下室人用我们白话说,太把自己当回事了。他自我标榜以为是文化分子,瞧不上一切人却又想通过和有权势却不和自己交好的人获得自尊心。甚至在自尊心受辱的时候靠欺辱一个可怜的失足女,站在道德制高点夸大并且把她的痛苦解构来弥补自己可怜的自尊心
他自以为一身书生气,但是他又深刻解剖自己内心的丑陋,他是庸才,只是自以为懂的思考,地下室人的以己度人,地下室人的可怜的自尊心,导致他永远得不到救赎
丽莎看出了他的痛苦,丽莎就是现实,是和索妮娅一样悲惨生活中的光点。但是地下室人别扭高傲,愤怒。他把自己对自己的自尊心受挫的折磨和自己导致自己产生的难堪,化为攻击性不断地投射到仆人,老同学和丽莎身上
他就是俄国版的阿q和孔乙己,陷入自我否定和寻求认同和存在感的精神困境中。不断地寻找哲学命题希望解构自己,躲在地下室里避开真正的生活。于是避开了丽莎,最后浑浑噩噩做着cos文化人的小市民美梦
实际上,我们总能从地下室人的精神状态中寻找到我们现代人的焦虑。我们的身份认同。我们在大千世界中获得了教育却高不成低不就,在底层挣扎的内心写照。我们纵使没有地下室人的疯狂,也多多少少有现代的身份认同焦虑,希望获得存在感和认同,希望得到爱和关注。
我想说,地下室人滑稽可笑自私傲慢,但是可怜可耻可悲,是时代的造物是书生气在庸才身上让他发疯的具象化,是我们自我解剖自我认识自我改变自我救赎的标本

圣地亚哥·萨瓦拉 本书主人公,小萨
安布罗修·帕尔多 本书主人公之一,卡约的童年玩伴、司机,后又给小萨的父亲当司机
费尔民·萨瓦拉 小萨的父亲,大资产阶级,在冷酷无情的墙头草形象下却始终有一颗慈父的心
阿玛莉亚·塞尔达 费尔民和奥登西娅家的佣人,后成为安布罗修的妻子
蒂蒂 小萨的妹妹
奇斯帕斯 小萨的哥哥
卡约·贝尔穆德斯 内政部办公厅主任,秘鲁独裁统治的代行人
奥登西娅 别名”缪斯“,卡约的情妇
凯妲 妓女,奥登西娅的朋友
波佩耶·阿雷瓦洛 圣地亚哥的朋友同时也是妹夫

前言

第一次读略萨的小说,真是名副其实。现实主义小说,常常读,但是第一次接触结构现实主义小说,着实惊艳人。

几近白描的描写,在我心中却赫然超过了以大多意识流见长的小说,以现实主义见长的略萨,又以他的结构现实主义笔触讲述了一个庞大,横贯数年的关于独裁下这一代人深受残害的故事。

在酒吧长谈中,我们能见到荒淫无度十指不沾阳春水被包养的女演员又怜爱地帮助她身边的女仆,能看到打手逐渐放弃良心,能看到少爷小萨与资产阶级的家庭决裂却又因没有真正的信仰而逐渐落入凡俗沉沦下去,可以看到坚毅的秘鲁共产党人执着勇敢的坚持理想,可以看到群像中的芸芸众生,他们做恶他们行善,他们幡然悔悟他们含恨而死

故事以主角圣地亚哥萨瓦拉的视角展开,生活较为拮据的圣地亚哥被妻子告知,家里宠物被捉狗大队捉住,在前去领狗的过程中无意间碰到了曾经父亲的司机。

——安布罗修·帕尔多,”老爷,您是圣地亚哥少爷?“。于是俩人在廉价酒吧把酒言欢,分别谈论起两个人这些年的故事

略萨就此将两个人的视角拆开,将完整的故事分割,以对话波的形式将两个人的经历从最初开始联系起来,在两个人的对话波中时常插入以阿玛莉亚为核心的故事。三条主线徐徐展开,时而感觉毫无关联,时而互为解释。

黑暗之前

小萨你是从什么时候开始倒霉的呢,你总是在想这个问题。

或许是当你出生在一个充满家人之爱的大资本家的家庭的时候,是当你比哥哥妹妹都聪明讨父亲喜爱的时候,是你思考世界探索个人意志的时候。但是不管如何你曾经不过是一个与波佩耶捉弄家中仆人的淘气孩子

这个笼罩在秘鲁数年的阴云一开始也和你没有关系。

彼时尚年轻的卡约爱上了贫贱的赫尔特鲁迪丝,在安布罗修的帮助下,卡约卑鄙地娶了这位贫穷的小姐。可终究是荷尔蒙一时的涌动,两个人秘密而简陋的婚礼引来了父亲的不满,父子之间的隔阂使他们再未相见。而赫尔特鲁迪斯也并不是真正中了卡约的计,而是为了攀上卡约刻意而为之的装傻

于是年份轮转,时间更迭,卡约和他的妻子老去步入中年

浑浑噩噩和不爱的妻子几乎可能要共读一生的卡约迎来了他人生的伯乐,但是是发掘恶的伯乐。卡约嫌弃他的妻子变得愈发不美貌(实际上在最开始也并不美貌),于是一去不回,成为了即将到来的秘鲁黑暗时代独裁者的左右手,作为奥德利亚最凶狠的爪牙控制这个时代。

——“没想到你也学会了托关系啊,安布罗修”

黑暗中的凡人

阿玛莉亚·塞尔达,她曾经深爱着安布罗修,可这个男人却深深伤害了她。安布罗修隐藏他们的关系,尤其在对阿玛莉亚的雇主金球也就是我们的主人公小萨的父亲时,更不敢暴露两人的关系。性格敏感而追求强烈而真诚的爱意的女人阿玛莉亚在被费尔民辞退后,也和安布罗修说了再见

此时此刻,住进了姨妈家的阿玛莉亚在新工作中认识了果敢大胆的工人特立尼达,他心思粗犷但是声音洪亮爱意涛涛如水,在几次不断的试探中,阿玛莉亚和他相爱了。

爱意涛涛可只是热恋中轰轰烈烈承诺过的爱意,时间快速的抹平这位年轻人的爱意,逐渐让他变得暴戾,甚至对赚钱养家的阿玛莉亚动手。在最后因为他的政治活动愚蠢地死去,作为流氓阿普拉党的最后陌路即使是堕落与可耻,也让人唏嘘深感讽刺。

阿玛莉亚在打击中也失去了他们的孩子,黑暗侵蚀着底层群众,让他们勇敢有斗志又让他们愚昧,让他们不自觉跌入罗列好的罪名和陷阱。

黑暗中的理想主义者

小萨此时还是青年,作为家中唯一的高等知识分子(被家人亲切的戏称为超级学者),这也是家人对他身处大资产阶级家庭却不切实际追求理想主义,不切实际的书生气的一种调侃表现吧

家里人想让小萨去贵族们云集的天主教学校,可小萨却希望进入圣马可学校,作为小萨一生中最为重要的转折点。小萨恋爱了,小萨本就是理想主义者,可是小萨的理想主义虚无缥缈。正如小萨本人后来所说

”我这辈子一直想信仰某种东西,“圣地亚哥说道,”然而却一直在撒谎,我没有信仰“

小萨的理想到底是什么呢,是一种寄托,是小萨出生在大资产阶级家庭却习得一身书生气的必然。小萨遇见了阿伊达和哈珂沃。阿伊达是坚定的共产主义者,于是小萨作为虚伪的理想主义者的载体找到了。小萨也不清楚自己是否真的信仰于此,但是小萨在爱意的驱使下开始进行学生活动

在阿伊达和哈柯沃在小萨不心甘情愿地撮合在一起后,小萨心态产生了变化,在参会和运动时都变得痛苦。

小萨你是从这时开始倒霉的吧,你因为年少不可得之爱情,最后没有选择坚定理想。

小萨你是从这时开始倒霉的吧,你因为最后一次运动被捕,最后竟与伙伴分道扬镳。

小萨当你在警察局,你有钱的老爸,你扶植了奥德利亚的大资本家父亲把你带了回去,你的朋友们却仍在遭受牢狱之苦。你没有履行承诺用你的影响力闹大这件事。小萨你或许就是从这时倒霉的吧。

小萨你的父亲真的生气了,他不是因为你搞赤色,只是想让你先安心学习。他生气也是因为他正在被政治上的敌人前文中的堂卡约监视,你却大摇大摆地打家里的电话而让父亲身陷囹圄

你或许是从这时开始倒霉的吧?

可是你的父亲从未生气,他察觉到你的态度之后立刻安慰你原谅你向你道歉。你是他最爱也是最担心的儿子。

小萨可是你从这时开始倒霉了,你感觉你的理想主义是假的,你感觉一切依赖父亲,你感觉你背叛了你的朋友,你的内心被愧疚胆怯和小资产阶级的懦弱充盈满了呀!

于是你向父亲讨要了一份报社的工作,谎称还会去上学,就此和父亲展开了长达数年的冷战

缪斯谋杀案

妓女和情妇的友谊

阿玛莉亚此刻折转到了卡约的情妇——奥登西娅,也就是缪斯的家中做女仆

这个女人是多么可爱啊,她和阿玛莉亚用笑话调笑,她并不矜持也满口的脏话,她和许多人来往。

她不像索伊拉,那么的不近人情呢。她有一个好朋友凯妲,妓女。她们常常一起打电话捉弄人。

凯妲也是一位很悲惨可怜的女性,但是她们此刻笑得多欢快啊。

阿玛莉亚也在此时又与安布罗修重归于好。

在堂卡约倒台之后,阿玛莉亚依旧选择帮助奥登西娅给她做女仆,即使奥登西娅越发贫困,尽管奥登西娅被爱情欺骗,即使她是个十足的蠢女人

在阿玛莉亚怀孕之后,在她生育之后,她在想。天啊这个悉心照料日夜帮助的女人竟然都不来看看她的孩子,不来看看小奥登西娅!

可是——

奥登西娅已经被残酷地杀死了

记者与妓女的谈话

此时多年未回家未联系父亲的小萨,你决定去调查这件事的蹊跷

你找到了凯妲,从她口中得知了你的父亲与安布罗修之间千丝万缕的关系,她笃定,安布罗修就是你的父亲费尔民派出残忍地杀害了阿玛莉亚的。

小萨你此刻想起了家庭和父亲,你告诉了父亲凯妲的猜测,你是从此刻开始倒霉的吗?

你终于和父亲母亲联系了,你开始每周回一次家,和家里终于开始联系。

金球和他的司机

缪斯一直在勒索费尔民,用她在卡约当政时得到的那些费尔民不干净的事…

可她不知道费尔民和他的司机安布罗修的事情,多么可怜可耻。

费尔民和安布罗修的通奸,费尔民怎么能知道安布罗修已经有了妻子呢。安布罗修也不敢让费尔民知道,与其说不敢,他甚至在可怜这个人,这个没得到过爱的人。

一个投机倒把的资本家,一个秘鲁的蛀虫,但是在安布罗修的眼里却变得可怜。他或许不忍让费尔民知道他的事让他伤心

甚至为了费尔民亲自残忍地杀了缪斯…

你是从什么时候开始倒霉的

小萨最后因为结婚和家里的争吵,最后也没有见到费尔民的弥留之际。

他伤心的不是费尔民的死亡

因为人人都会死

他伤心的是,直到死前

费尔民都以为他们大吵了一架

费尔民是这片大地仇恨和邪恶的扶植者,是见风使舵的投机者,即使是死后的遗产都在想着规避税款。但是他对安布罗修流露出了脆弱与敏感,流露出了自卑与悲伤。在儿子面前流露出了希望与教导,对儿子的胆怯,担心他再也不回来的想法深深困住了他的后半生,也许这个恶人的柔情散尽,全部倾注在他的小儿子身上了吧

小萨拒绝了哥哥的提议——父亲的一切遗产。他选择用偏执和一种近似自虐的固执来坚持自己虚假的理想主义,即使口袋掏空也靠自己生活,即使过着这样的生活,小萨也坚持了他的理想主义,小萨其实理想主义会倒霉,虚假的理想主义也会倒霉,你的父亲会倒霉,那些沉沦在黑暗现实中的人也会倒霉,秘鲁这片深藏苦难惆怅的大地才是你倒霉的根源

安布罗修最后把小奥德西娅交给了其他人看护,自己踏上了寻找工作浑浑噩噩的堕落之路,他回答圣地亚哥他不知道未来,他现在要做的就是

干了这一杯吧!我要继续去抓狗了——