数据结构入门——线性表、堆栈和队列
文章目录
前言
本系列文章将简要介绍数据结构课程入门知识,文章将结合我们学校(吉大)数据结构课程内容进行讲述。文中算法大部分来自朱允刚老师上课的讲解,朱老师是我遇到最认真负责的老师,很有幸能成为朱老师的学生。
一、线性表的链接存储
线性表相信大家都很熟悉了,直接跳过定义和顺序存储部分来讲讲线性表的链接存储。
注:顺序存储可以看看这篇文章 数据结构之顺序表
链表中的操作和实现的代码可以看这篇数据结构之单链表详解
链接存储: 用任意一组存储单元存储线性表,一个存储单元除包含结点数据字段的值,还必须存放其逻辑相邻结点(前驱或后继结点)的地址信息,即指针字段
单链表
单链表的定义:每个结点只含一个链接域的链表叫单链表
链表的第一个结点被称为头结点(也称为表头),指向头结点的指针被称为头指针(head). 链表的最后一个结点被称为尾结点(也称为表尾),指向尾结点的指针被称为尾指针(tail)
为了对表头结点插入、删除等操作的方便,通常在表的前端增加一个特殊的表头结点,称其为哨位(哨兵)结点 。
循环链表
定义:在单链表中,使尾结点的指针指回到哨位结点,称这样的单链表为循环链表。
循环链表使我们可从链表的任何位置开始,访问链表中的任一结点。
双向链表
每个节点有两个指针域
左指针指向其前驱,右指针指向其后继;
优点:方便找节点的前驱。
顺序存储和链式存储的比较
1.空间效率的比较
顺序表所占用的空间来自于申请的数组空间,数组大小是事先确定的,当表中的元素较少时,顺序表中的很多空间处于闲置状态,造成了空间的浪费;
链表所占用的空间是根据需要动态申请的,不存在空间浪费问题,但链表需要在每个结点上附加一个指针,从而产生额外开销。
2.时间复杂性的比较
基于下标的存取 | 插入和删除 | |
---|---|---|
顺序表 | O(1) 按下标直接存取 | O(n) 需要移动若干元素 |
链表 | O(N) 从头开始遍历链表 | O(1) 只修改几个指针值 |
静态链表
静态链表作为一种编程技巧,在有指针的程序设计语言中,也有广泛的应用
链表经典问题实例
(一)已知一个带有表头结点的单链表,假设该链表只给出了头指针list。在不改变链表的前提下,请设计一个尽可能高效的算法,查找链表中倒数第k个位置上的结点(k为正整数)。若查找成功,算法输出该结点的data值,并返回1;否则,只返回0。
解法一(暴力): 找到最后一个节点向前找k-1次前驱
解法二: 倒数第k个即正数第n-k+1个
解法三(双指针): 使用两个指针:p和q,先把p指向第k个元素,然后p和q同时向后遍历,当p遍历到结尾时,q正好遍历到倒数第k个。
解法三用到的双指针法是链表问题中常用的解法在学习中应重点掌握 |
---|
(二)给定两个单链表的头指针head1和head2,设计一个算法判断这两个链表是否相交,如果相交则返回第一个交点,要求时间复杂度为O(L1+L2),L1、L2分别为两个链表的长度。为了简化问题,这里我们假设两个链表均不含有环.
思路: 是否相交?如果两个链表相交,则最后一个结点一定是共有的,可以分别遍历2个链表,记录其最后一个结点和链表长度。若2个链表最后一个结点相等,则相交,否则不相交。
找相交结点?用指针p1指向较长的那个链表,p2指向较短的那个链表,p1先向后移动|L1-L2|步,然后p1和p2同时向后移动,每移动一步比较p1和p2是否相等,当二者相等时,其指向的结点即为交点。
这里只选取了两个经典问题进行分析,目的是使大家了解到双指针法在链表问题中的应用。
更多例题可以看这篇文章 链表常见面试题
关于链表中的跳表的问题可能会另开一章介绍😊
二、堆栈和队列的应用
这里默认大家对于堆栈和队列的基本操作都了解,不了解请看这两篇文章
数据结构之栈详解
数据结构之队列详解
栈的应用
-
操作系统是如何实现函数(递归)调用的?
Windows等大部分操作系统中,每个运行中的程序都配有一个调用栈。
借助该栈可以跟踪属于同一程序的所有函数,记录他们之间的相互调用关系,保证在每一调用执行完毕后可以准确返回。 -
如何记录调用与被调用函数间的关系?如何实现函数(递归)调用的返回?
一次函数调用所需要的信息表示为一个工作记录(帧):
返回地址
参 数 (函数名、引用参数与实参等)
局部变量
函数调用时执行入栈操作保存本次调用对应的信息
函数返回时执行出栈操作恢复上次调用对应的信息
进制转换
将十进制正整数转换成其它进制数
例:十进制转换成八进制:(66)10=(102)8
66/8=8 余 2
8/8=1 余 0
1/8=0 余 1
结果为余数的逆序:102。先求得的余数在写出结果时最后写出,最后求出的余数最先写出,符合栈的后出先进性质,故可用栈来实现数制转换。
括号匹配
高级语言程序设计中的各种括号应该匹配,例
如:“(” 与 “)”匹配、
“[”与 “]” 匹配、
“{”
与 “}” 匹配等。
根据匹配规则:后遇到的左括号先匹配;
扫描字符串,采用栈存放左括号,当遇到右括号时,和栈顶的左括号进行匹配;
栈只存左括号。
★算术表达式求值
算术表达式求值是高级语言在编译过程中的一个基本问题。它的实现是栈的一个典型应用
算术表达式的表示方法
- 中缀表达式--运算符在操作数之间
如: A * B / C
运算规则:
(1) 先计算括号内,后计算括号外;
(2) 在无括号或同层括号内,先进行乘除运算,后
进行加减运算,即乘除运算的优先级高于加减
运算的优先级;
(3) 同一优先级运算,从左向右依次进行。 - 后缀表达式(逆波兰式)
定义:运算符紧跟在两个操作数之后的表达式。
优点:①后缀表达式没有括号
② 不存在优先级的差别
③ 计算过程完全按照运算符出现的先后次序进行
后缀表达式求值的算法
设置一个栈,存放操作数
从左到右依次读入后缀表达式的每一个操作数/运算符/结束符:
- 若读到的是操作数,将它压入堆栈
- 若读到的是运算符,就从堆栈中连续弹出两个元素(操作数),进行相应的运算,并将结果压入栈中。
- 读入结束符时,栈顶元素就是计算结果。
中缀表达式转后缀表达式算法
设置一个栈,存放运算符
从左到右依次读入中缀表达式的每一个元素:
➢ 操作数规则:直接放入后缀表达式
➢ 运算符规则:
- 栈空或栈顶是左括号:运算符压栈
- 当前运算符优先级> 栈顶运算符:压栈
- 当前运算符优先级≤栈顶运算符:弹栈直至当前运算符优先级大于栈顶或栈空或栈顶为左括号,再压栈
➢ 括号规则:
- 遇到左括号:压栈
- 遇到右括号:弹栈至左括号
➢ 结束符规则:弹栈至栈空
栈混洗
给定一个有序的数据元素的序列,通过控制入栈和出栈的时机,可以得到不同的出栈序列,这就是栈混洗。
栈混洗甄别
对于出栈序列中1 ≤ i < j < k ≤ n 位置的3个元素
···,ai,···,aj,···,ak,···
如果ai > ak > aj
则必非合法出栈序列。
栈混洗总数
n个元素栈混洗总数即卡特兰(明安图)数
Catalan(n)=
1
n
+
1
\frac{1}{n+1}
n+11
C
2
n
n
C^{n}_{2n}
C2nn
队列的应用
凡数据符合先进先出特性的问题,可考虑队列