小言_互联网的博客

数据结构精录&总结Episode.3 数据结构入门之栈和队列详解(基于Visual C++)

348人阅读  评论(0)

通信作业做了一整个晚上,突然想起自己今天忘记更新博文了。。难顶。。

然而做信息论作业的时候,我突然想起之前和小组成员研究香农编码方法的C语言实现时,正好就用到了队列算法的一点点思想解决编码过程中反复迭代对比的工序,这也许是一种巧合,更多的还是证明了最最基本的数据结构算法在工程上的强大应用潜力。

之前记得看到过一篇IT杂志上的文章,讲的是拥有十年从业经验的顶级高校毕业的软件工程师,面试新公司时被要求说出刚进大学时初学的数据结构知识却哑口无言的故事。这就好比我们虽然拥有了Matlab也要学习数值计算,拥有了仿真软件也要学习中考高考的电路题一样。知识可以被集成而简化使用,但是不会被架空。


提到栈与队列,不管是参加期末考试、考研还是应聘面试,不能不熟记这些知识点:

一、定义:

栈(Stack)是一个后进先出(Last in first out,LIFO)的线性表,它要求只在表尾进行删除和插入操作

队列是限定只能在表的一端进行插入,在表的另一端进行删除的特殊的线性表

二、特点:

栈:

1、栈是特殊的线性表(顺序表、链表),它在操作上有一些特殊的要求和限制:栈的元素必须“后进先出”

2、栈的表尾称为栈的栈顶(top),相应的表头称为栈底(bottom)

3、栈的操作只能在这个线性表的表尾进行

队列:

4、队列是先进先出(FIFO, First-In-First-Out)的线性表

5、队列只允许在后端(称为rear)进行插入操作,在前端(称为front)进行删除操作

三、区别:

栈和队列是两种实现完全不同目的和功能的结构,同时他们的操作本质也不同例如我们在说存储方面“堆栈”肯定不会说成“堆队列”

1、队列只允许新数据在后端进行添加

2、栈先进后出,队列先进后出

 

栈对于学习计算机的人来说,是再熟悉不过的东西了,很多东西都需要用栈来存储,像是操作系统就会给每个线程创建一个栈用来存储函数调用时各个函数的参数,返回地址及临时变量等,函数本身也有一个函数栈用来存储函数的局部变量等。

栈的特点就是后进先出,需要O(N)的时间才能找到栈中的最值。

队列和栈刚好相反,是先进先出,表面上和栈是一对矛盾体,但实际上,都可以利用对方来实现自己。

关于栈和队列的具体代码实现,我们将栈和队列分为顺序、链式四种类型,每种类型都有不同的结构体定义及函数操作,以下代码完整地进行了总结,同时知识要点也在注释中给出,已经过VS2019测试成功运行。


  
  1. // 第三章 栈和队列.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。编写—JoeyBG,代码算法和程序排版上还有不足之处,敬请见谅
  2. //
  3. #include <iostream>
  4. #include<iomanip>
  5. #include<string.h>
  6. #include<math.h>
  7. #include<time.h>
  8. using namespace std;
  9. #define Maxsize 100 //空间分配定义常数,这里可以用于栈和队列的初始建立;
  10. // 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单
  11. // 调试程序: F5 或调试 >“开始调试”菜单
  12. // 入门使用技巧:
  13. // 1. 使用解决方案资源管理器窗口添加/管理文件
  14. // 2. 使用团队资源管理器窗口连接到源代码管理
  15. // 3. 使用输出窗口查看生成输出和其他消息
  16. // 4. 使用错误列表窗口查看错误
  17. // 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目
  18. // 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件
  19. /*
  20. 栈和队列一些易考知识点小结:
  21. (1)栈、队列的定义及其相关数据结构的概念,包括:顺序栈,链栈,共享栈,循环队列,链队等。栈与队列存取数据(请注意包括:存和取两部分)的特点。
  22. (2)递归算法。栈与递归的关系,以及借助栈将递归转向于非递归的经典算法:n!阶乘问题,fib数列问题,hanoi问题,背包问题,二叉树的递归和非递归遍历问题,图的深度遍历与栈的关系等。其中,涉及到树与图的问题,多半会在树与图的相关章节中进行考查。
  23. (3)栈的应用:数值表达式的求解,括号的配对等的原理,只作原理性了解,具体要求考查此为题目的算法设计题不多。
  24. (4)循环队列中判队空、队满条件,循环队列中入队与出队(循环队列在插入时也要判断其是否已满,删除时要判断其是否已空)算法,其中条件可简要总结如下:
  25. 【循环队列的队空队满条件:】
  26. 为了方便起见,约定:初始化建空队时,令
  27. front=rear=0,
  28. 当队空时:front=rear,
  29. 当队满时:front=rear 亦成立,
  30. 因此只凭等式front=rear无法判断队空还是队满。
  31. 有两种方法处理上述问题:
  32. (1)另设一个标志位以区别队列是空还是满。
  33. (2)少用一个元素空间,约定以“队列头指针front在队尾指针rear的下一个位置上”作为队列“满”状态的标志。
  34. 队空时: front=rear,
  35. 队满时: (rear+1)%maxsize=front
  36. 循环队列的主要操作:
  37. (1)创建循环队列
  38. (2)初始化循环队列
  39. (3)判断循环队列是否为空
  40. (4)判断循环队列是否为满
  41. (5)入队、出队
  42. */
  43. //顺序栈的建立及操作代码
  44. typedef struct SqStack {
  45. int* base; //栈底指针
  46. int* top; //栈顶指针
  47. }SqStack; //顺序栈的结构体定义
  48. bool InitStack(SqStack& S)
  49. {
  50. S.base = new int[Maxsize]; //为顺序栈分配一个最大容量为Maxsize的空间
  51. if (!S.base) //空间分配失败
  52. return false;
  53. S.top = S.base; //top初始为base,空栈
  54. return true;
  55. } //顺序栈的初始化代码
  56. bool Push(SqStack& S, int e)
  57. {
  58. if (S.top - S.base == Maxsize) //栈满
  59. return false;
  60. *(S.top++) = e; //元素e压入栈顶,然后栈顶指针加1,等价于*S.top=e; S.top++;
  61. return true;
  62. } //插入新的栈元素,由于栈是一个只允许头进头出的结构,因而元素e只能放在栈顶,但是不要忘记栈顶指针+1
  63. bool Pop(SqStack& S, int& e)
  64. {
  65. if (S.base == S.top) //栈空
  66. return false;
  67. e = *(--S.top); //栈顶指针减1,将栈顶元素赋给e
  68. return true;
  69. } //栈删除元素的操作函数,这里只能在栈的头部进行元素的剔出,暂存在变量e中,不要忘记栈顶指针-1的操作
  70. int GetTop(SqStack S)
  71. {
  72. if (S.top != S.base) //栈非空
  73. return *(S.top - 1); //返回栈顶元素的值,栈顶指针不变
  74. else
  75. return -1;
  76. } //取得S的栈顶元素函数,相对于栈删除元素唯一的不同在于栈顶指针不变
  77. //链栈的操作代码详解
  78. typedef struct Snode {
  79. int data; //数据域
  80. struct Snode* next; //指针域
  81. }Snode, * LinkStack; //链栈的结构体定义
  82. bool InitStack(LinkStack& S)
  83. {
  84. S = NULL;
  85. return true;
  86. } //链栈的初始化构造函数,直接先将头节点赋NULL即可
  87. bool Push(LinkStack& S, int e)
  88. {
  89. LinkStack p;
  90. p = new Snode; //生成新结点
  91. p->data = e; //将e放在新结点数据域
  92. p->next = S; //将新结点的指针域指向S,即将S的地址赋值给新结点的指针域
  93. S = p; //修改栈顶指针为p
  94. return true;
  95. } //在栈顶插入元素的函数,这里我们是将新节点指向S,意味着栈虽然头进头出,但是指针链接的方向是反向的
  96. bool Pop(LinkStack& S, int& e)
  97. {
  98. LinkStack p;
  99. if (S == NULL) //栈空
  100. return false;
  101. e = S->data; //将栈顶元素赋给e
  102. p = S; //用p保存栈顶元素地址,以备释放
  103. S = S->next; //修改栈顶指针,指向下一个结点
  104. delete p; //释放原栈顶元素的空间
  105. return true;
  106. } //栈删除元素的操作函数,这里只能在栈的头部进行元素的剔出,暂存在变量e中,不要忘记释放原栈顶元素的空间,类似于上述顺序栈头指针-1操作
  107. int GetTop(LinkStack S)
  108. {
  109. if (S != NULL) //栈非空
  110. return S->data; //返回栈顶元素的值,栈顶指针不变
  111. else
  112. return -1;
  113. } //取得S的栈顶元素函数,相对于栈删除元素唯一的不同在于不用释放任何空间
  114. //由以上四个函数的操作可以看出,链栈和链表本质上是相同的两个东西,只不过在操作上栈只能对头节点进行增删工作,实则是比链表更简单的数据结构
  115. //循环队列的操作代码详解
  116. typedef struct SqQueue {
  117. int* base; //基地址
  118. int front, rear; //头指针,尾指针
  119. }SqQueue; //循环队列的结构体定义
  120. bool InitQueue(SqQueue& Q)//注意使用引用参数,否则出了函数,其改变无效
  121. {
  122. Q.base = new int[Maxsize]; //分配空间
  123. if (!Q.base) return false;
  124. Q.front = Q.rear = 0; //头指针和尾指针置为零,队列为空
  125. return true;
  126. } //循环队列的初始化操作函数
  127. bool EnQueue(SqQueue& Q, int e)//将元素e放入Q的队尾
  128. {
  129. if ((Q.rear + 1) % Maxsize == Q.front) //尾指针后移一位等于头指针,表明队满
  130. return false;
  131. Q.base[Q.rear] = e; //新元素插入队尾
  132. Q.rear = (Q.rear + 1) % Maxsize; //队尾指针加1
  133. return true;
  134. } //循环队列的入队,从尾指针处插入一个元素,同时头指针等于尾指针后移一位是队列满,不要忘记队尾指针需要模Maxsize才是步数
  135. bool DeQueue(SqQueue& Q, int& e) //删除Q的队头元素,用e返回其值
  136. {
  137. if (Q.front == Q.rear)
  138. return false; //队空
  139. e = Q.base[Q.front]; //保存队头元素
  140. Q.front = (Q.front + 1) % Maxsize; //队头指针加1
  141. return true;
  142. } //循环队列的出队函数,删除头指针元素,头指针+1移动一位且用e暂存值
  143. int GetHead(SqQueue Q)//返回Q的队头元素,不修改队头指针
  144. {
  145. if (Q.front != Q.rear) //队列非空
  146. return Q.base[Q.front];
  147. return -1;
  148. } //取循环队列的队头元素函数,需要判断队列非空的时候才能进行操作
  149. int QueueLength(SqQueue Q)
  150. {
  151. return (Q.rear - Q.front + Maxsize) % Maxsize;
  152. } //循环队列的长度获取函数,直接返回长度即可,同样不要忘记模Maxsize才是步数,否则物理地址不是按一个二进制数or寄存器对应一个结构体来存的
  153. //链队列操作代码
  154. typedef struct Qnode {
  155. int data;
  156. struct Qnode* next;
  157. }Qnode, * Qptr; //链队列的结构体定义
  158. typedef struct {
  159. Qnode* front;
  160. Qnode* rear;
  161. }LinkQueue; //链队列的二级总控(相当于同时存储头尾指针的一个封装)
  162. void InitQueue(LinkQueue& Q)//注意使用引用参数,否则出了函数,其改变无效
  163. {
  164. Q.front = Q.rear = new Qnode; //创建头结点,头指针和尾指针指向头结点
  165. Q.front->next = NULL;
  166. } //链队列的初始化函数
  167. void EnQueue(LinkQueue& Q, int e)//将元素e放入队尾
  168. {
  169. Qptr s;
  170. s = new Qnode;
  171. s->data = e;
  172. s->next = NULL;
  173. Q.rear->next = s; //新元素插入队尾
  174. Q.rear = s; //队尾指针后移
  175. } //链队列的入队函数,新元素插入队尾,不要忘记队尾指针的移动
  176. bool DeQueue(LinkQueue& Q, int& e) //删除Q的队头元素,用e返回其值
  177. {
  178. Qptr p;
  179. if (Q.front == Q.rear) //队空
  180. return false;
  181. p = Q.front->next;
  182. e = p->data; //保存队头元素
  183. Q.front->next = p->next;
  184. if (Q.rear == p) //若队列中只有一个元素,删除后需要修改队尾指针
  185. Q.rear = Q.front;
  186. delete p;
  187. return true;
  188. } //链队列的出队操作函数,从头出去,不要忘记释放空间,这与循环队列只改指针指向不同
  189. int GetHead(LinkQueue Q)//返回Q的队头元素,不修改队头指针
  190. {
  191. if (Q.front != Q.rear) //队列非空
  192. return Q.front->next->data;
  193. return -1;
  194. } //取链队列的头元素函数,这里相对于删除是不会释放任何空间的
  195. //各栈或队列的调用总函数及程序的主函数接口
  196. int SqStackmain()
  197. {
  198. int n, x;
  199. SqStack S;
  200. InitStack(S); //初始化一个顺序栈S
  201. cout << "请输入元素个数n:" << endl;
  202. cin >> n;
  203. cout << "请依次输入n个元素,依次入栈:" << endl;
  204. while (n--)
  205. {
  206. cin >> x; //输入元素
  207. Push(S, x);
  208. }
  209. cout << "元素依次出栈:" << endl;
  210. while (S.top != S.base) //如果栈不空,则依次出栈
  211. {
  212. cout << GetTop(S) << "\t"; //输出栈顶元素
  213. Pop(S, x); //栈顶元素出栈
  214. }
  215. return 0;
  216. } //顺序栈的总调用函数
  217. int LinkStackmain()
  218. {
  219. int n, x;
  220. LinkStack S;
  221. InitStack(S); //初始化一个顺序栈S
  222. cout << "请输入元素个数n:" << endl;
  223. cin >> n;
  224. cout << "请依次输入n个元素,依次入栈:" << endl;
  225. while (n--)
  226. {
  227. cin >> x; //输入元素
  228. Push(S, x);
  229. }
  230. cout << "元素依次出栈:" << endl;
  231. while (S != NULL) //如果栈不空,则依次出栈
  232. {
  233. cout << GetTop(S) << "\t"; //输出栈顶元素
  234. Pop(S, x); //栈顶元素出栈
  235. }
  236. return 0;
  237. } //链栈的总调用函数
  238. int SqQueuemain()
  239. {
  240. SqQueue Q;
  241. int n, x;
  242. InitQueue(Q); //初始化队列(一定要初始化,否则后面存储出错)
  243. cout << "请输入元素个数n:" << endl;
  244. cin >> n;
  245. cout << "请依次输入n个整型数,依次入队:" << endl;
  246. while (n--)
  247. {
  248. cin >> x;
  249. EnQueue(Q, x); //入队
  250. }
  251. cout << endl;
  252. cout << "队列内元素个数,即长度:" << QueueLength(Q) << endl;
  253. cout << "队头元素:" << GetHead(Q) << endl;
  254. cout << "元素依次出队:" << endl;
  255. while ( true) //如果队列不空,则依次出队
  256. {
  257. if (DeQueue(Q, x))
  258. cout << x << "\t"; //出队元素
  259. else
  260. break;
  261. }
  262. cout << endl;
  263. cout << "队列内元素个数,即长度:" << QueueLength(Q) << endl;
  264. return 0;
  265. } //循环队列的总调用函数
  266. int LinkQueuemain()
  267. {
  268. LinkQueue Q;
  269. int n, x;
  270. InitQueue(Q); //初始化队列(一定要初始化,否则后面存储出错)
  271. cout << "请输入元素个数n:" << endl;
  272. cin >> n;
  273. cout << "请依次输入n个整型数,依次入队:" << endl;
  274. while (n--)
  275. {
  276. cin >> x;
  277. EnQueue(Q, x); //入队
  278. }
  279. cout << "队头元素:" << GetHead(Q) << endl;
  280. cout << "元素依次出队:" << endl;
  281. while ( true) //如果栈不空,则依次出栈
  282. {
  283. if (DeQueue(Q, x))
  284. cout << x << "\t"; //出队元素
  285. else
  286. break;
  287. }
  288. cout << endl;
  289. return 0;
  290. } //链队列的总调用函数
  291. int main()
  292. {
  293. int judgenumber = 0;
  294. char judge = '\n';
  295. cout << "栈和队列学习笔记+总结代码本—编写&调试:JoeyBG,算法尚有大量不足之处,还望不吝指正!" << endl;
  296. cout << "------------------------------------------------------------------------------------" << endl;
  297. cout << endl;
  298. labelstart:
  299. cout << "1、顺序栈" << endl;
  300. cout << "2、链栈" << endl;
  301. cout << "3、循环队列" << endl;
  302. cout << "4、链队列" << endl;
  303. cout << endl;
  304. cout << "键入你需要的栈或队列类型数字:";
  305. cin >> judgenumber;
  306. if (judgenumber == 1)
  307. {
  308. system( "cls");
  309. SqStackmain();
  310. }
  311. else if (judgenumber == 2)
  312. {
  313. system( "cls");
  314. LinkStackmain();
  315. }
  316. else if (judgenumber == 3)
  317. {
  318. system( "cls");
  319. SqQueuemain();
  320. }
  321. else if (judgenumber == 4)
  322. {
  323. system( "cls");
  324. LinkQueuemain();
  325. }
  326. else
  327. {
  328. cout << "键入数字有误!" << endl;
  329. }
  330. cout << endl;
  331. cout << "是否继续查看其它链表或者退出(Y/N)?";
  332. cin >> judge;
  333. if (judge == 'Y')
  334. {
  335. system( "cls");
  336. goto labelstart;
  337. }
  338. else if (judge == 'N')
  339. {
  340. system( "cls");
  341. exit( 0);
  342. }
  343. else
  344. {
  345. cout << "键入字符有误,系统默认退出!" << endl;
  346. exit( 0);
  347. }
  348. return 1;
  349. } //主函数,进行初始界面及人工选择数据结构的工作
  350. /*
  351. 参考文献:
  352. 1、速度七十迈:软考初级程序员常考知识点汇总(2)线性表和栈与队列,http://www.51mokao.com/forumpostscont.html?id=177082
  353. 2、陈小玉:趣学数据结构,人民邮电出版社,2019.09
  354. */

北京理工大学徐特立学院乐学平台关于栈和队列数据结构给出了四道程序编写练习题,依照以上总结的栈和队列操作方式,我们给出如下的代码解答:

Q1.输出学生成绩

试设计一个算法,建立一个学生成绩栈要求从键盘上输入法 N 个整数,按照下列要求分别进入不同栈,并分别输出每个栈的内容。

(1) 若输入的整数 x 小于 60 ,则进入第一个栈;

(2) 若输入的整数 x 大于等于 60 ,小于 100 ,则进入第二个栈;

(3) 若输入的整数 x 大于等于 100 ,则进入第三个栈;


  
  1. #include<iostream>
  2. #include<queue>
  3. using namespace std;
  4. void pusha(queue<int>a)
  5. {
  6. while(!a.empty())
  7. {
  8. cout<<a.front()<< " ";
  9. a.pop();
  10. }
  11. cout<< endl;
  12. }
  13. int main()
  14. {
  15. queue< int>r,s,t;
  16. int N,x;
  17. cin>>N;
  18. while(N--)
  19. { cin>>x;
  20. if(x>= 100) t.push(x);
  21. else if(x< 60) r.push(x);
  22. else s.push(x);
  23. }
  24. cout<< "60>x:";pusha(r);
  25. cout<< "60<=x<100:";pusha(s);
  26. cout<< "x>=100:";pusha(t);
  27. return 0;
  28. }

利用C++STL标准模板库中的队列模板省掉了复杂的函数重写工作,但是其本质与上述知识相符,详细也可参看编译器&工作站中的<queue>头文件具体代码内容。

以下是queue模板库的常用成员函数对应功能:

①size() 返回队列中元素的个数

②empty() 如果队列空则返回真 

③back() 返回最后一个元素引用即队尾

④front() 返回第一个元素引用即队首

⑤pop() 删除第一个元素,即队首元素。不返回 

⑥push() 在末尾加入一个元素,即放置在队尾 。不返回

 

Q2.括号是否配对

假设一个算术表达式可以包含三种括号:“(”和“)”,方括号“[”和“]”,及花括号“ { ”和“ } ”,且这三种括号可嵌套使用。试设计算法判断给定表达式中所含括号是否配对出现。


  
  1. #include<iostream>
  2. #include<queue>
  3. using namespace std;
  4. int main()
  5. {
  6. queue< char>s;
  7. char x,i= 1;
  8. double a= 0;
  9. while((x=getchar())!= '\n'&&i!= 0)
  10. {
  11. s.push(x);
  12. if(x== '{'||x== '['||x== '(') {a+=x/(++i);}
  13. else if(x== '}'||x== ']') {a-=(x -2)/(i--);}
  14. else if(x== ')') {a-=(x -1)/(i--);}
  15. }
  16. if(a== 0)
  17. cout<< "括号配对"<< endl;
  18. else cout<< "括号不配对"<< endl;
  19. }

仍然是queue模板库的使用,但是分析这道题,和我们非常熟悉的队列应用“回文算法”如出一辙,换了个说法而已。

 

Q3.奇偶数输出

编写一个程序,任意输入n个100以内的数,将它们的奇数和偶数分别存入链队为Q1和Q2中,然后配对输出链队Q1、Q2中的值,直到任一队列为空为止。


  
  1. #include<iostream>
  2. #include<queue>
  3. using namespace std;
  4. int main()
  5. {
  6. queue< int>Q1,Q2;
  7. int n,x;
  8. cin>>n;
  9. while(n--)
  10. {
  11. cin>>x;
  12. if(x% 2== 0) Q2.push(x);
  13. else Q1.push(x);
  14. }
  15. while(!Q1.empty()&&!Q2.empty())
  16. {
  17. cout<<Q1.front()<< " "<<Q2.front()<< endl;
  18. Q1.pop();Q2.pop();
  19. }
  20. return 0;
  21. }

 

Q4.回文

利用栈和队列的特性设计一个算法,用于判断一个字符串是否为回文。


  
  1. #include<iostream>
  2. #include<stack>
  3. #include<queue>
  4. using namespace std;
  5. int main()
  6. {
  7. stack< char> s;
  8. queue< char> t;
  9. char c;
  10. int i= 0;
  11. while((c=getchar())!= '\n')
  12. {
  13. s.push(c);
  14. t.push(c);
  15. i++;
  16. }
  17. while(i--)
  18. {
  19. if(s.top()==t.front())
  20. {s.pop(); t.pop(); }
  21. else { cout<< "不"; break;}
  22. }
  23. cout<< "是回文"<< endl;
  24. return 0;
  25. }

回文问题是最最基础的算法实例了,在刚进大学时学习C语言程序设计已经多次接触到了它。这一次我们同时利用栈和队列模板库重写了这个算法,在代码的简洁度易读性上比C语言中首位指标缩进比对方式优化许多。


转载:https://blog.csdn.net/weixin_43833550/article/details/105718871
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场