《啊哈算法》第二章栈,队列,链表(解析+代码)
创始人
2024-05-11 15:30:41
0

从无到有学算法 

吾日三省吾身,今天有写代码乎?🙄

🙄抠门渣男语录:我的果汁分你一半(月亮弯弯 绵绵绵绵缠缠) - 李金源 - 单曲 - 网易云音乐

千年之后的你在哪里:星月神话 - 我觉得我还能再吃点 - 单曲 - 网易云音乐 

目录

第一节 解密QQ号--队列 

第二节 解密回文--栈

第三节 纸牌游戏--小猫钓鱼

题目

思路 + 代码

 完整代码

输入输出

第四节 链表 

基础概念

实现链表

 完整代码1

插入数据

 完整代码2

第五节 模拟链表


先补充个理解:

 1,栈中的“先进后出,后进先出”什么意思

栈类似于弹匣,就像装子弹压弹进弹匣一样,一粒一粒压进去,但是退子弹是从最上面开始的,最先压进去的最后弹出来,最后一个压进去的第一个弹出来,这就是“先进后出”,也叫“后进先出”

2,栈的定义

 栈就是数据暂时存储的地方,所以才有进栈,出栈的说法

3,栈与队列的区别

栈是把子弹子弹压进弹匣,后进的先出;队列就像排队,你第一个排,那就第一个轮到你,这就是先进先出,也叫后进后出。dfs(深度优先搜索)应用栈,而bfs(广度优先搜索)应用队列。栈占用空间小,而堆中的bfs常用于最短路。

4,栈在计算机领域的解释

栈作为一种数据结构,是只能在一端进行插入和删除的特殊线性表,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始读。允许进行插入和删除操作的一段称为栈顶(top),另一端称为栈底(bottom)。栈底固定,而栈顶浮动;栈中元素个数为0时成为空栈。插入数据称为进栈(PUSH),删除数据称为退栈(POP),栈也称后进先出表。栈在函数调用时可以可以存储断点,做递归时要用到栈!

5,堆和栈的区别

栈快捷但自由度小,堆过程长但自由度大

第一节 解密QQ号--队列 

介绍

队列是一种特殊的线性结构,遵循“先进先出”,First In First Out,FIFO原则

它是BFS(Breadth First Search广度优先搜索)和队列优化的Bellman-Ford最短路算法的核心数据结构

比如排队买票,打电话中的人工服务,都是队列的例子,来的越早的人,越早买到票并离开

队首(head)删除数据,称之为“出队”

队尾(tail)插入数据,称之为“入队”

当队列中没有元素时(head == tail),称为空队列

题目

小哼问小哈QQ号是多少,小哈是个大美女,小哈给了他一串加密后的数字,解密规则是:

先将第1个数删除,然后把第2个数放到这串数末尾,再将第3个数删除,并把第4个数放到末尾......

直到剩下最后一个数,把最后一个数也删除。把删除的数连在一起就是小哈的QQ号了。小哈给他加密过的一串数是“920430714”,求小哈的QQ号是多少?

解析

队首删除一个数的操作是:head++;

队尾增加一个数的操作是:q[tail] = x; tail++;

现在有9个数,把9个数放入队列后,head == 1,tail == 10,此时head和tail之间的数就是目前队列中“有效”的数

代码1

#include
using namespace std;
int main()
{int q[102] = {0, 9, 2, 0, 4, 3, 0, 7, 1, 4}; //q[0]是额外加上的int head = 1, tail = 10; //head指向队首,tail指向队尾下一个位置while(head < tail) {//队列不为空cout<
903744102

代码2

将队列三个基本元素(一个数组,两个变量)封装为一个结构体类型

代码臃肿(本题不必用结构体),只是为了敲多一遍加深印象

#include
using namespace std;
struct que
{int a[100]; //队列主体,存储数据int head; //队首int tail; //队尾
};
int main()
{struct que q;q.head = 1;q.tail = 1;for(int i = 1; i <= 9; ++i) {cin>>q.a[q.tail]; //在队尾插入数据q.tail++;}while(q.head < q.tail) {//队列不为空cout<
9 2 0 4 3 0 7 1 4
903744102

第二节 解密回文--栈

队列先进先出,栈后进先出,生活中的例子有:

1,吃桶装薯片,得先吃完前面的才能吃到最后一片

2,浏览网页退回之前某个网页,需要一步步点击后退键

3,给弹夹装子弹,最后装入的子弹是第一个被打出去的,或者说第一个被退出来的

4,又比如一个小桶,小桶的直径只能放入一个球,依次放入2,1,3号小球,想取出2号球就得先将3号取出来,再将1号取出来,最后才能取最先放进去的2号小球

 下面我们用栈的思路判断回文:

只需要一个一维数组a[110]保存读入的数据

一个一维数组s[110]作为栈的主体和一个指向栈顶的变量top就可以

初始化top = 0

入栈的操作:s[++top] = a[i];

通过strlen(a)得到字符串长度len,mid = len / 2 - 1;

代码第14行 s[++top] = a[i]; 为什么用++top呢,为了使后续top = 1(最后一个字符)后,再 top--,即top = 0时可判断为YES

第8行 cin.get(a, 110);  等价于 gets(a),都可读入字符数组的字符串,不过后者头文件为#include,前者头文件为#include

#include
#include //strlen(), cin.get()
using namespace std;
int main()
{char a[110], s[110];int len, mid, next, top;cin.get(a, 110); //读入字符串len = strlen(a); //求长度mid = len / 2 - 1;top = 0;for(int i = 0; i <= mid; ++i)s[++top] = a[i]; //mid前的字符入栈if(len % 2 == 0) next = mid + 1;else next = mid + 2; //奇偶匹配的起始下标不一样//开始匹配for(int i = next; i <= len - 1; ++i) {if(a[i] != s[top])break;top--;}if(top == 0) cout<<"YES"<
aininia
YESnihuai
NO

下面是不用栈的思路,它需要一个数组a和两个变量left, right

#include
#include //cin.get(), strlen()
using namespace std;
char a[666];
bool repeat(int left, int right) //判断回文
{int flag = 1;while(left < right) {if(a[left++] == a[right--]) continue;else flag = 0; //出现不相等}return flag;
}
int main()
{cin.get(a, 666);int right = strlen(a) - 1;if(repeat(0, right)) cout<<"YES"<
bananab
YESbababb
NO

栈还可验证括号匹配,end~

第三节 纸牌游戏--小猫钓鱼

本题结合了栈和队列

我们需要两个队列,一个栈来模拟整个游戏

题目

寒假到了,小哼和小哈出来约会,他们来到了一家咖啡馆玩桌游。

这款桌游是一个叫“小猫钓鱼”的扑克游戏:

1,

将一份扑克牌平均分两份,每人一份。小哼先拿出了自己手中的第一张扑克牌放到桌上,然后小哈也拿出自己受手中的第一张扑克牌,并放在小哼刚打出的扑克牌上面,就像这样两人交替出牌

2,

出牌时,如果某人打出的牌与桌上某张牌牌面相同,即可将两张相同的牌及中间所夹的牌全部取走,并依次放到自己手中牌的末尾

3,

当任意一人手中的牌全部出完,游戏结束,对手获胜,

假设游戏开始,小哼有6张牌,小哈也有6张,且牌的牌面只有1~9

思路 + 代码

(以下思路中,存在游戏永远无法结束的情况,只是简陋版本)

容易得知,胜负从拿到牌开始就确定了,且桌上每种牌面最多一张

创建一个结构体来实现队列

struct queue
{int data[1000];int head;int tail;
};

 head存储队首,tail存储队尾,数组data存储队列元素,当然大小的设置可以不止1000,数组不越界就好

创建一个结构体来实现栈

struct stack
{int data[10];int top;
};

 top存储栈顶,数组data存储栈元素,大小设为10,因为只有9种不同牌面,考虑到打出的牌与桌面相同则可取走,桌上最多只能有9张牌,所以10够用了

定义队列变量q1, q2,q1模拟小哼的牌,q2模拟小哈的牌,定义栈变量s模拟桌上的牌

struct queue q1, q2;
struct stack s;

 初始化队列和栈

//初始化q1, q2为空,此时小哼小哈手中无牌
q1.head = 1; q1.tail = 1;
q2.head = 1; q2.tail = 1;
//初始化栈s为空,最开始桌上无牌
s.top = 0;

 分别读入小哼,小哈的牌,每次读入6个数,分别插入q1, q2

for(int i = 1; i <= 6; ++i) {cin>>q1.data[q1.tail]; //读入一个数到队尾q1.tail++; //队尾往后挪一位
}
for(int i = 1; i <= 6; ++i) {cin>>q2.data[q2.tail]; q2.tail++;
}

 小哼出牌

t = q1.data[q1.head]; //小哼亮出一张牌

 小哼打出第一张牌,即q1队首,将这张牌存放在临时变量t种,接下来判断桌上有无与t相同的牌

用一个数组记录桌上的牌,由于牌面只有1~9,开一个大小为10的数组即可

int book[10];

 将book[1]~book[9]初始化为0,因为开始桌上无牌

for(int i = 0; i <= 9; ++i)book[i] = 0;

 如果桌上多了一张牌面2的牌,则book[2] = 1,因为牌面为2的牌桌上有了。若牌面2的牌被拿走,就将book[2]设置为0,表示桌上没有牌面2的牌了,此时只需一个if来判断桌上是否有某张牌(类似桶排序)

t = q1.data[q1.head]; //小哼先出牌
if(book[t] == 0) { //桌面没有牌面为t的牌,小哼本轮没有赢牌q1.head++; //已打出的牌出队s.top++; //注意top初始为0s.data[s.top] = t; //已打出的牌入栈,即放到桌面book[t] = 1; //标记桌上有牌面t的牌
}

如果不满足book[t] == 0,小哼可以赢得桌上的牌,将赢得的牌依次放入小哼手中 

else {q1.head++; //小哼已出牌,所以要把这张牌出队q1.data[q1.tail] = t; //可赢牌,就把刚打出的牌放回手中牌的末尾q1.tail++;while(s.data[s.top] != t) { //桌上可赢的,依次放到手中末尾q1.data[q1.tail] = s.data[s.top]; //依次放入队尾q1.tail++;s.top--; //栈种少了一张牌,栈顶减1}
}

 以上,就是小哼出牌阶段,小哈类似,只是q1变q2

在模拟两人出牌外,加一个while循环判断是否有人出完牌

while(q1.head < q1.tail && q2.head < q2.tail) 
//队列q1, q1都不为空,双方都有牌,继续循环

 最后,输出winner,以及获胜者手中的牌 和 桌上的牌

若小哼获胜,小哈手中无牌,此时q2.head == q2.tail

if(q2.head == q2.tail) {cout<<"小哼win"< 0) {//若桌上有牌,依次输出cout<<"桌上的牌是";for(i = 1; i <= s.top; ++i)cout<

 完整代码

#include
using namespace std;
struct queue
{int data[1000]; //队列的主体,表示玩家的牌int head; //队首int tail; //队尾
};
struct stack
{int data[10]; //栈的主体,表示桌上的牌int top; //栈顶
};
int main()
{struct queue q1, q2; //队列变量struct stack s; //栈变量int book[10];int i, t;//初始化队列q1.head = 1; q1.tail = 1;q2.head = 1; q2.tail = 1;//初始化栈s.top = 0;//初始化标记桌上牌的数组for(i = 1; i <= 9; ++i)book[i] = 0;//依次向队列插入小哼的6张牌for(i = 1; i <= 6; ++i) {cin>>q1.data[q1.tail];q1.tail++;}//依次向队列插入小哈的6张牌for(i = 1; i <= 6; ++i) {cin>>q2.data[q2.tail];q2.tail++;}while(q1.head < q1.tail && q2.head < q2.tail) { //队列不为空t = q1.data[q1.head]; //小哼出的第一张//判断当前能否赢if(book[t] == 0) {//桌上没有这个牌面的牌q1.head++; //打出的牌出队s.top++;s.data[s.top] = t; //打出的牌入栈book[t] = 1; //标记桌上有了牌面t的牌}else { //桌上有这个牌q1.head++; //已打出的出队q1.data[q1.tail] = t; //打出的牌放到末尾q1.tail++;while(s.data[s.top] != t) {//可赢的牌book[s.data[s.top]] = 0; //取消标记q1.data[q1.tail] = s.data[s.top]; //依次放入队尾q1.tail++;s.top--; //桌上少了一张,栈顶减1}//遇到牌面与打出一样的,跳出上面的whilebook[s.data[s.top]] = 0;q1.data[q1.tail] = s.data[s.top];q1.tail++;s.top--;}if(q1.head == q1.tail) break; //牌出完,游戏结束t = q2.data[q2.head]; //小哈出的第一张//判断当前能否赢if(book[t] == 0) {//桌上没有这个牌面的牌q2.head++; //打出的牌出队s.top++;s.data[s.top] = t; //打出的牌入栈book[t] = 1; //标记桌上有了牌面t的牌}else { //桌上有这个牌q2.head++; //已打出的出队q2.data[q2.tail] = t; //打出的牌放到末尾q2.tail++;while(s.data[s.top] != t) {//可赢的牌book[s.data[s.top]] = 0; //取消标记q2.data[q2.tail] = s.data[s.top]; //依次放入队尾q2.tail++;s.top--;}//遇到牌面与打出一样的,跳出上面的whilebook[s.data[s.top]] = 0;q2.data[q2.tail] = s.data[s.top];q2.tail++;s.top--;}}if(q2.head == q2.tail) {cout<<"小哼win"< 0) { //桌上有牌就输出cout<<"桌上的牌是 ";for(i = 1; i <= s.top; ++i)cout< 0) { //桌上有牌就输出cout<<"桌上的牌是 ";for(i = 1; i <= s.top; ++i)cout<

输入输出

2 4 1 2 5 6
3 1 3 5 6 4
小哈win
小哈当前手中的牌是 1 6 5 2 3 4 1
桌上的牌是 3 4 5 6 21 6 2 8 5 5
3 4 6 7 3 2
小哈win
小哈当前手中的牌是 2 6 2 4 6 3 5 7 8 3
桌上的牌是 1 57 8 9 1 2 3
6 4 2 3 3 8
小哼win
小哼当前手中的牌是 3 3 2 4 1 9 3 8 2
桌上的牌是 7 6 8

 跟着书本敲完一遍,好像学到了什么,又好像什么都没学到  🙄(只能说有了印象,大概理解)

由于是简陋版本,只能通过部分测试数据,这个程序显然是不够“健壮”的

第四节 链表 

只通过啊哈算法里讲的那一点点,是不够的,所以我通过网上的资源加深理解

要善用网上资源,网络不是只让你打游戏,撩妹😂,娱乐的

 1,blibli(1.5倍速) (40分钟)

1个小时学会单链表,C语言数据结构专题_哔哩哔哩_bilibili

2,blibli(1.5倍速) (15分钟)

2.4链表_哔哩哔哩_bilibili

3,CSDN(30分钟) 链表基础知识详解(非常详细简单易懂)_不秃也很强的博客-CSDN博客_链表

然后我开始结合自己的理解,照着书本敲关键知识点(90分钟)

基础概念

1,

链表由一个个节点构成,每个节点采用结构体的形式组织,它是一种物理存储上非连续,通过指针连接的线性存储结构

2,

链表与数组的区别:

数组通过开辟连续的内存来存储数据,有起始和结束地址;

而链表通过对节点的插入和删除实现对数据的读取,没有头尾之分

使用链表可以更灵活地对数据进行插入和删除

3,

链表由结构体变量(节点)相连构成,每一个节点由数据域和指针域组成,数据域存放具体数值,指针域存放下一节点首地址

4,

结构体中的"."和"->"可以读作“的”,比如p->next读作p的next,p->data读作p的data,用来访问结构体成员,区别是,指针变量用"->",一般变量用"."访问成员

实现链表

如何实现链表呢?使用指针和动态分配内存函数malloc来实现

指针: 

 指针中的 * 表示取内容或者说解引用,& 表示取地址,比如

int a; cin>>a; int *p; p = &a; cout<<*p;

&将a变量的地址赋值给指针p,指针p表示a变量的地址

*使p取得a所指向地址的值,*p表示a变量的地址所指向的值

malloc函数:(头文件#include

1,malloc(sizeof(int))

等价于 malloc(4); 从内存中申请分配4个字节大小的内存空间

2,

现在已经成功从内存中申请了4个字节的空间来存放一个整数,如何对这个空间操作呢?

这就需要一个指针来指向这个空间,即存储这个空间的首地址

3,(int *)

malloc()函数返回类型使void *类型,表示未确定类型的指针,该类型可强制转换为任何其他类型指针,此处我们需要强转整型,所以是(int *)

int *p; //定义指针p
p = (int *)malloc(sizeof(int));

malloc()函数动态申请空间:

#include
#include //malloc()
using namespace std;
int main()
{int *p; //定义指针p//指针p获取动态分配的内存地址p = (int *)malloc(sizeof(int));*p = 10; //向p所指向的内存存入10cout<<*p<
10

 每一个节点由两部分组成,左边部分存放具体数值,右边部分存储下一个节点的地址(称为后继指针),这里我们定义一个结构体类型来存储这个节点

struct node //节点node
{int data; //数据域struct node *next; //指针域,存放下一节点首地址
};

 

 上面代码中,我们定义了一个叫node的结构体类型,它有俩成员,第一个成员叫data,用来存储具体的数值;第二个成员是一个指针,用来存储下一节点的地址。因为下一节点的类型也是struct node,所以这个指针的类型也必须是struct node *类型的指针。

如何建立链表呢?首先需要一个头指针head指向链表的最开始,链表未建立时头指针为空(可理解为指向空节点) 

struct node *head;
head = NULL; //头指针初始为空

 现在创建第一个节点,并用临时指针p指向这个节点

struct node *p;
p = (struct node *)malloc(sizeof(struct node));
//使指针p变成了结构体变量

 设置第一个节点的左半部分和右半部分

int a;
cin>>a;
p->data = a; //将a的值存储到节点的数据域中
p->next = NULL; //后继指针为空,即下一节点为空

 

 可以这样念“p的data等于a,p的next为空”,->是结构体指针运算符,用来访问内部成员

设置头指针:(方便从头遍历整个链表) 

1,如果这是第一个创建的节点,头指针指向这个节点

2,如果不是,上一节点的后继指针指向这个节点

 

if(head == NULL)head = p; //第一个节点被头指针指向
elseq->next = p; //否则上一节点后继指针指向这里

最后将指针p也指向当前节点,因为下一步临时指针p会指向新节点 

q = p; //指针q指向当前节点

 完整代码1

#include
#include //malloc()
using namespace std;
struct node
{int data;struct node *next;
};
int main()
{struct node *head, *p, *q, *t;int i, n, a;cin>>n;head = NULL; //头指针初始为空for(i = 1; i <= n; ++i) {cin>>a;//动态申请一个空间存放节点,临时指针p指向这个节点p = (struct node *)malloc(sizeof(struct node));p->data = a; //数据存储到数据域p->next = NULL; //后继指针指向空if(head == NULL)head = p; //第一个节点让头指针指向elseq->next = p; //否则上一节点后继指针指向当前节点q = p; //上一节点指针指向当前节点}//输出链表中所有数t = head;while(t != NULL) {cout<data<<" ";t = t->next; //继续下一节点}return 0;
}
9
2 3 5 8 9 10 18 26 32
2 3 5 8 9 10 18 26 32

注意,上述代码没有free(释放动态申请的空间),很不安全 

插入数据

接下来,往链表插入6

 

关于插入,这里有个关键: 

下方完整代码第38,39行,初始指针t指向5,p指向6,t的next指向8

  1. p->next = t->next;

  2. t->next = p;

这两步相当于链表断开,接上了一个新的元素

p的next指向原来的t的next(指向了8的地址)

t的next再指向p的地址,顺序不能调换

插入后,t的next指向p的地址,p的next指向8的地址

 首先用临时指针t从链表头部开始遍历

t = head; //从链表头部开始遍历

 等指针t下一节点的值比6大,将6插到两节点中间,即t->next->data大于6时插入

t->next->data跟我念“t的next的data

cin>>a; //读入待插入的数
while(t != NULL) {//未到达链表尾部if(t->next == NULL || t->next->data > a) {//动态申请空间,存放新节点p = (struct node *)malloc(sizeof(struct node));p->data = a;//新增节点后继指针指向当前节点后继指针所指向的节点p->next = t->next;t->next = p; //当前节点后继指针指向新增节点break;}t = t->next; //继续下一节点
}

 完整代码2

#include
#include //malloc()
using namespace std;//创建一个结构体表示节点
struct node
{int data;struct node *next;
};int main()
{struct node *head, *p, *q, *t; //p,q,t都是临时指针int i, n, a;cin>>n; //n个数head = NULL; //头指针初始为空for(i = 1; i <= n; ++i) {cin>>a;//动态申请空间存放一个节点,临时指针p指向该节点p = (struct node *)malloc(sizeof(struct node));p->data = a;p->next = NULL; //当前节点下一节点为空if(head == NULL)head = p; //若为第一个创建的,头指针指向该节点elseq->next = p; //上一节点后继指针指向当前节点q = p; //指针q指向当前节点}cin>>a; //待插入的数t = head; //从链表头部开始遍历while(t != NULL) {if(t->next == NULL || t->next->data > a) {p = (struct node *)malloc(sizeof(struct node));p->data = a;//新增节点后继指针指向当前节点后继指针所指向的节点p->next = t->next;t->next = p; //当前节点后继指针指向新增节点break; //插入完退出循环}t = t->next; //继续下一节点}//输出链表所有数t= head;while(t != NULL) {cout<data<<" ";t = t->next; //继续下一节点}return 0;
}
9
2 3 5 8 9 10 18 26 32
6
2 3 5 6 8 9 10 18 26 32

耗时3.5小时终于完成了第四节的学习

第五节 模拟链表

模拟链表不需要指针,却能起到链表的作用,模拟链表还能实现双向链表和循环链表

学习模拟链表可以加深我们对链表的理解,因为它们的思想是一样的,只是工具不同

它使用一个数组存放序列中的数字,使用另一个数组存放下一数字的下标

 

 比如上面两个数组,

right[1] == 2表示数组data第1个元素右边元素的下标为2,

right[9] == 0表示数组data第9个元素右边元素不存在

 现在需要将6插入5和8中间,怎么做呢?只需要

data[10] = 6; //加在最后

right[3] = 10; //3号右边的元素存放在data[10]

right[10] = 4; //10号元素右边的元素存放在data[4]

 

完整代码 

#include
using namespace std;
int main()
{int data[110], right[110];int n;cin>>n;for(int i = 1; i <= n; ++i)cin>>data[i]; //读入数据for(int i = 1; i <= n; ++i) { //初始化数组rightif(i != n) right[i] = i + 1;else right[i] = 0;}cin>>data[n + 1]; //待插入数//从链表头部开始遍历int t = 1;while(t != 0) {if(data[right[t]] > data[n + 1]) {right[n + 1] = right[t]; //right[10] = right[3]right[t] = n + 1; //right[3] = 10break; //插入完跳出循环}t = right[t]; //t自增至满足条件或遍历完}//输出链表所有数t = 1; //类似链表中 指针t = head;//t = 1少了会从中间开始输出while(t != 0) {cout<
9
2 3 5 8 9 10 18 26 32
6
2 3 5 6 8 9 10 18 26 32

代码中 data[n + 1] == 6 ,当满足插入条件时,t == 3, right[n + 1] = 4, right[t] == 10

相关内容

热门资讯

监控摄像头接入GB28181平... 流程简介将监控摄像头的视频在网站和APP中直播,要解决的几个问题是:1&...
Windows10添加群晖磁盘... 在使用群晖NAS时,我们需要通过本地映射的方式把NAS映射成本地的一块磁盘使用。 通过...
protocol buffer... 目录 目录 什么是protocol buffer 1.protobuf 1.1安装  1.2使用...
在Word、WPS中插入AxM... 引言 我最近需要写一些文章,在排版时发现AxMath插入的公式竟然会导致行间距异常&#...
【PdgCntEditor】解... 一、问题背景 大部分的图书对应的PDF,目录中的页码并非PDF中直接索引的页码...
Fluent中创建监测点 1 概述某些仿真问题,需要创建监测点,用于获取空间定点的数据࿰...
educoder数据结构与算法...                                                   ...
MySQL下载和安装(Wind... 前言:刚换了一台电脑,里面所有东西都需要重新配置,习惯了所...
修复 爱普生 EPSON L4... L4151 L4153 L4156 L4158 L4163 L4165 L4166 L4168 L4...
MFC文件操作  MFC提供了一个文件操作的基类CFile,这个类提供了一个没有缓存的二进制格式的磁盘...