栈
当n个不同元素进栈时,出栈元素不同排列的个数为
$$
\frac{1}{n+1}C_{2n}^{n} = \frac{(2n)!}{(n+1)!n!}
$$
该公式为卡特兰数(Catalan)公式
栈的输出序列
如果输入次序是顺序输入,可以观察最先一个输出的元素,若最先输出的是最后输入的元素,则输出序列一定是倒序且唯一
共享栈
栈底位置相对不变,让两个顺序栈共享一个一维数组空间,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间中间延申
共享栈更有效利用空间,两个栈空间相互调节,只有当整个存储空间均被占满才会出现栈空间上溢出
栈的基本操作
void InitStack(&S); //初始化
void GetTop(S,&x); //读栈顶元素
bool Pop(&S,x); //出栈
bool Push(&S,x); //入栈
bool StackEmpty(S); //判空操作
void DestroyStack(&S); //销毁操作
任何应用场景只要涉及最后加入的部分,最先执行的过程,都适合使用栈去实现,即LIFO
栈的应用——括号匹配
算法过程
- 遍历括号序列,遇到左括号压入栈中
- 每遇到右括号,栈顶元素弹出,与右括号进行匹配
- 若栈目前为空,结束遍历并认定序列非法
- 若弹出元素与右括号为同种括号则继续,反之直接结束遍历并认定序列非法
- 括号序列遍历完成,若栈不为空,则说明序列非法
栈的应用——表达式求值
前/中/后缀表达式:运算符在两个操作数前面/中间/后面
前缀表达式=波兰表达式
后缀表达式=逆波兰表达式
两个操作数如果没有运算先后顺序,中缀表达式转换为前缀/后缀表达式可能有多个结果
- 后缀表达式,运算符号出现的先后次序,与中缀表达式的符号运算顺序相同
同时为了保证算法结果唯一性(运算次序手算与机器计算结果相同),采用“左优先”原则:只要坐标的运算符能先计算,就先按计算左边
算法过程
((15/(7-(1+1))x3)-(2+(1+1)))
中缀转后缀
15 7 1 1 + - / 3 x 2 1 1 + + -
- 中缀表达式根据运算规则以及左优先原则,转为后缀表达式
- 后缀表达式从左往右遍历,遇到操作数直接压栈
- 每遇到一个运算符,运算符最近的两个数出栈(先出栈的是右操作数),进行运算后结果作为新的一个操作数再压入栈中
- 若无法弹出两个数,则说明表达式非法
- 表达式遍历完成后,若只剩下一个操作数,则说明表达式合法,该操作数则为表达式结果
中缀转前缀表达式,则采用“右优先”原则
表达式从右往左遍历,其余和上述方法一致,注意进行预算结合时,先出栈的是左操作数
栈的应用——表达式转化
中缀表达式转后缀表达式
注意该算法栈空间需要预留足够,可能会出现栈溢出问题
算法过程:
- 中缀表达式从左往右遍历,遇到操作数直接加入后缀表达式
- 遇到左括号直接压栈,遇到右括号逐个将栈内元素弹出加入后缀表达式,直至弹出左括号,同时左右括号不加入后缀表达式
- 遇到运算符,逐个将栈内优先级高于或等于当前运算符的所有运算符弹出并加入后缀表达式,若过程中遇到左括号或者栈为空则停止,最后将当前运算符入栈
中缀表达式计算(表达式转化+表达式求值)
- 初始化两个栈,A栈用于存放操作数,B栈用于存放运算符
- 中缀表达式从左往右遍历,遇到操作数压入A栈
- 遇到操作符或者括号,将栈内优先级高于或等于当前运算符的运算符依次弹出,每弹出一个运算符从A栈弹出两个操作数进行运算,并将结果压回A栈。(括号匹配同表达式转化算法)
栈的应用——递归
- 适合使用递归算法解决的问题,一般是具有可将原始问题规模转换为属性相同,但规模更小的问题的解决思路
- 递归算法具有递归表达式以及边界条件
- 递归算法具有很高的空间复杂度,因此在使用该算法时注意考虑栈溢出情况
- 递归算法也可以自定义栈的方式,把递归算法转变为非递归算法
- 递归算法可能会包含很多重复计算
队列
队列和栈的逻辑结构相同,两者的区别在于删除、插入操作的限定规则不同
一般情况下,链式队列的队头都是放在链表头部,方便出队。
链式队列空间虽然是动态分配的,但是不代表长度不受限制,长度仍受内存空间限制
链式队列和顺序队列出队、入队时间复杂度均为O(1),均可以顺序访问,但链式队列无法通过队首、队尾指针确定队列长度,顺序队列则可以
链式队列出队操作通常只要修改头指针,但是当队列只有一个元素且发生删除时,需要修改尾指针rear=front
队列的基本操作
void InitQueue(&Q); //初始化
void GetHead(Q,&x); //读队头元素
bool DeQueue(&Q,&x); //出队
bool EnQueue(&Q,x); //入队
bool QueueEmpty(Q); //判空操作
队列的应用
- 树层次遍历
- 图广度优先遍历
- 操作系统中应用
- 多个进程争夺有限系统资源时,FCFS/FIFO先来先服务是一种常用策略。
- 通过设置一个队列缓冲区,让任务在缓冲区中以先进先出的形式,有序执行
双端队列
队列队头、队尾均可以进行插入、删除操作的数据结构,常考察输出序列的合法性。
双端队列还有两种衍化结构:
- 输出受限的双端队列
一端可以插入、删除,但是另一端只能插入的双端队列
- 输入受限的双端队列
一端可以插入、删除,但是另一端只能删除的双端队列
双端队列可以模拟为一个堆栈,因此栈能够输出的序列,双端队列也能够输出
数组与特殊矩阵
-
一维数组,个数组元素大小相同,且物理上连续存放,因此具有随机存储特性
- 假定一维数组起始地址为LOC,b[i]元素的存储地址为
LOC+i*N*sizeof(ElemType)
- 考研题目中,数组默认下标以0开始,除非题目特殊说明
- 假定一维数组起始地址为LOC,b[i]元素的存储地址为
-
二维数组的存储方式可采用行优先存储以及列优先存储,从而具备随机存储特性
- 假定二维数组起始地址为LOC
- 按行优先存储,b[i][j]元素的存储地址为
LOC+(i*N+j)*sizeof(ElemType)
- 按列优先存储,b[i][j]元素的存储地址为
LOC+(j*M+i)*sizeof(ElemType)
-
普通矩阵可以按照二维数组进行存储
- 在描述矩阵元素时,行、列号通常从1开始,而描述数组时通常下标从0开始,需要注意
- 普通存储空间规模为N*N
压缩存储的目的就是只存储非0元素,其余0元素不占用空间。
一般题目,如果求压缩数组的下标通项公式,可采用特殊值带入求解
注意题目中元素下标是0开始还是1开始
对称矩阵,矩阵内任意a(i,j)=a(j,i)
- 对称矩阵压缩存储:只存储主对角线+下/上三角区元素
- 按照行优先元素存储到一维数组,共需要存储$\frac{n(n+1)}{2}$个元素,即最后一个元素的下标为$\frac{n(n+1)}{2}$-1
- 为了方便随机存取,按照行优先原则,假定存储主对角线以及下三角元素,当
i≥j
时,a(i,j)元素的下标为$\frac{i(i-1)}{2}$+j-1;当i<j
时,利用a(i,j)=a(j,i)即可获得该元素的值,即a(i,j)元素的下标为$\frac{j(j-1)}{2}$+i-1
- 为了方便随机存取,按照行优先原则,假定存储主对角线以及下三角元素,当
- 按照列优先元素存储到一维数组,数组大小仍然不变,但a(i,j)元素的下标则变为
(n+n-1+n-2+...+n-(j-1)+1)+(i-j)
上/下三角元素,除了主对角线和上/下三角区,其余元素均相同
- 压缩存储:三角区元素存储在一维数组中,额外增加一个位置存储常量
- 按照行优先元素存储到一维数组,共需要存储$\frac{n(n+1)}{2}$+1个元素
- 为了方便随机存取,按照行优先原则,对于下三角矩阵,当
i≥j
时,a(i,j)元素的下标为$\frac{i(i-1)}{2}$+j-1;当i<j
时,a(i,j)元素的下标均为$\frac{n(n+1)}{2}$ - 对于上三角矩阵,,当
i≥j
时,a(i,j)元素的下标为(n+n-1+n-2+...+n-(i-1)+1)+(j-i)
;当i<j
时,a(i,j)元素的下标均为$\frac{n(n+1)}{2}$
- 为了方便随机存取,按照行优先原则,对于下三角矩阵,当
三对角矩阵(带状矩阵,三边矩阵),矩阵内下标|i-j|>1
时元素值为0,即主对角线上的相邻元素可以为非0元素,其余元素值均为0
- 按行优先存储,除了第一行、最后一行只有2个元素,其余行均有3个元素,因此共需要存储
3n-2
个 - 当
|i-j|>1
,元素值直接放回0,这部分元素不需要存储在一维数组中 - 对于b[k]元素,他在数组中为第
k+1
个元素,前i-1
行共有3(i-1)-1
个元素,前i
行共有3i-1
个元素,因此3i-4≤k+1≤3i-1
,则满足$i≥\frac{k+2}{3}$,向上取整i=$⌈$(k+2)/3$⌉$即可(或向下取整i=$⌊$(k+2)/3+1$⌋$) - 同时有
k=2i+j-3
,所以第k+1
个元素的下标就已经获得了
稀疏矩阵,非零元素个数远少于矩阵元素个数
稀疏矩阵的特点是非零元素数量较少
稀疏矩阵无法更具矩阵元素行列下标快速定位矩阵元素,不支持随机存取
- 压缩存储方式1,通过顺序存储三元组<行,列,值>,但是无法进行随机存取
- 压缩存储方式2,十字链表法,具有两个指针域数组right、down,分别指向第i行的第一个元素、第j列第一个元素,每个非零数据节点包含行、列、数值域、指向同列下一个元素的指针以及指向同行下一个元素的指针