第四章 栈和队列
栈(stack)是限定只能在表尾进行插入和删除操作的线性表。把允许插入和删除的一端(即线性表的表尾)叫做栈顶(top),另一端叫做栈底(bottom);不含任何数据元素的栈称为空栈;栈又称后进先出的线性表,简称LIFO结构。栈的插入操作称为进栈,也称压栈或入栈;栈的删除操作称为出栈,也称弹栈;
注意:栈对线性表的插入和删除操作的位置进行了限制,但并未对元素进栈出栈的时间进行限制,不管是否所有元素都已经进栈,只要保证已经进栈的元素中,是处于栈顶位置的元素最先出栈就OK!
栈的顺序结构简称顺序栈,用数组来实现。数组下标为0的一端作为栈底较好,因为第一个元素都存储在栈底,变化最小所以作为栈底,定义变量top来表示栈顶元素在数组中的位置,top必须小于栈的长度;当栈只存储一个元素时,top=0;当栈为空栈时,判定条件为top=-1
/*顺序栈的进栈操作,即push,把数据d存入栈中*/
void push(Stack stack, DataType d) {
if (stack.top == stack.size-1) { // 栈满
return ERROR;
}
stack.top++; // 栈顶指针自加
stack.data[stack.top] = d; // 把数据d存入栈顶
}
/*顺序栈的出栈操作,即pop,弹出栈顶元素*/
DataType pop(Stack stack, DataType d) {
if (stack.top == -1) { // 空栈
return ERROR;
}
d = stack.data[top]; // 接收栈顶元素并返回
stack.top--; // 栈顶指针自减
return d; // 返回栈顶元素
}
两栈共享空间
我们用长度为n的数组来实现这两个栈的共享空间。让数组下标0位置是A栈的栈底位置,而数组下标(n-1)位置是B栈的栈底位置,这样一来两个栈如果增加元素,就是从数组的两端向中间延伸。
top1和top2为A栈和B栈的栈顶指针,当A栈为空时,top1=-1;当B栈为空时,top2=n;假想极端情况:当B栈为空时,top1=n-1则表示A栈已经栈满;当A栈为空时,top2=0表示B栈已经栈满。所以,当top1和top2相差1的时候就表示公共栈满了。所以栈满的判断条件为top1 + 1 = top2;
/*两栈共享存储空间的push操作,需要传入一个stackNmu参数来表示栈号,即向哪个栈执行push操作*/
boolean push(Stack statck, DataType d, int stackNum) {
if (stack.top1 + 1 == stack.top2) { // 栈满,不能再执行push
return false;
}
// 因为上面已经判断栈满的情况,所以执行push时不用担心内存溢出问题!
if (stackNum == 1) { // 栈1执行push操作
stack.data[++stack.top1] = d; // top1先自加,再给栈1的栈顶元素赋值
} else if (stackNum == 2) { // 栈2执行push操作
stack.data[--stack.top2] = d; // top2先自减,再给栈2的栈顶元素赋值
}
return true;
}
/*两栈共享空间的pop操作*/
DataType pop(Stack stack, DataType d, int stackNum) {
if (stackNum==1) {
if (statck.top1 == -1) { // 栈1为空,溢出
return ERROR;
}
d = stack.data[satck.top1--]; // 把栈1的栈顶元素出栈,top1自减
} else if (stackNum == 2) {
if (staack.top2 == n) { // 栈2为空,溢出
return ERROR;
}
d = stack.data[stack.top2--]; // 把栈2的栈顶元素出栈,top2自减
}
return d;
}
两栈共享存储空间是针对存储相同数据结构的数据而设计的,一般在两个栈的空间需求关系相反时才会这样做,也就是一个栈在增长时另一个栈在缩短的情况。
栈的链式存储结构及其实现
站的链式存储结构简称链栈,链栈的栈顶在单链表的头部,杜宇链栈来说基本不存在栈满的情况,除非内存已经没有可以使用的空间
/*链栈的进栈操作push,设新结点为s*/
void push(LinkedStack stack, DataType d) {
LinkedStackNode s = new LinkedStackNode(nu;;);
s.data = e;
s.next = stack.next;
stack.top = s; // 栈顶指针
stack.count++; // 栈中结点个数
}
/*链栈的出栈操作pop,设新结点p用来存储要删除的结点*/
DataType pop(LinkedStack stack, DataType d) {
LinkedStackNode p = new LinkedStack(null);
if (stack.isEmpty()) {
return ERROR;
}
d = stack.top.data; 接收要删除的结点数据
p = stack.top;
stack.top = stack.top.next; // 栈顶指针下移一位
/*free(p)*/
stack.count--; // 栈中数据个数减1
return d; // 返回删除的数据
}
顺序栈和链栈的使用场景:要存储的数据量变化很大不可预料的时候,最好使用链栈;如果存储的数据量在可控范围之内,则使用顺序栈会更好!
栈的作用:栈的引入简化了程序设计问题,划分了不同的关注层次,使得思考范围缩小,更加聚焦于我们要解决的核心问题。反之像数组等数据结构,因为要分散精力去考虑数组的下标增减等细节问题,反而掩盖了问题的本质。
/*递归实现斐波那契数列*/
public class FBNQ {
int Fbi(int i) {
if (i<2) {
return i == 0 ? 0 : 1;
}
return Fbi(i-1) + Fbi(i-2); // 递归调用
}
public static void main(String[] args) {
for (int i = 0; i<40; i++) {
System.out.println(Fbi(i));
}
return 0;
}
}
/*循环实现斐波那契数列*/
public void fbnq {
int[] arr = new int[40];
arr[0] = 0;
arr[1] = 1;
for (int i=2; i<40; i++) {
arr[i] = arr[i-1] + arr[i-2];
System.out.println(arr[i]);
}
}
递归
递归的定义:在高级语言中,调用自己和调用其它函数并没有本质的区别,我们把一个直接调用自己或者通过一系列调用语句,间接调用自己的函数,称作递归函数。每个递归定义必须至少有一个人条件,满足结束递归调用,即不再引用自身而是返回值退出。
对比以上两种实现斐波那契数列的代码,迭代循环和递归法实现的区别是:迭代使用的是循环结构,递归使用的是选择结构。递归代码的结构更加清晰、更简洁、更容易理解,从而减少读代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存,迭代法则不需要反复调用函数和占用额外内存,因此我们应该视不同的情况选择不同的代码实现方式。
递归过程分为前行阶段和回退阶段,递归过程的回退顺序是它前行顺序的逆序,在后退过程中要执行某些动作,包括回复在前行过程中存储起来的某些数据。这种存储某些数据,并在后面又以存储的逆序的回复这些数据的需求显然很符合栈数据结构,因此编译器就是使用栈时间递归调用的。简单说就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都会被压入栈中,在回退阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其他部分,也就是恢复了调用状态。
栈的应用,四则运算的逆波兰(后缀表示法)求值
比如四则运算(中缀表达式):9+(3-1)*3+10/2 的后缀表达式:9 3 1 - 3 * + 10 2 / + 所有的符号都要在运算数字的后面出现,运算规则是:从左到右遍历表达式的每个数字和符号,遇到是数字就进栈,遇到是符号就把位于栈顶的两个数字出栈并进行运算,运算结果进栈,一直到最终获得结果为止。
那么中缀表达式怎样转换为后缀表达式呢?规则:从左向右遍历中缀表达式中的每个数字、符号、左右括号,
- 若是数字就直接读到输出流中,即成为后缀表达式的一部分;
- 若是符号,第一个符号直接进栈,后面的符号则需要判断它和栈顶符号的优先级,若其优先级不高于栈顶符号优先级,则栈顶的优先级较高的符号以及优先级相同的符号出栈并进入输出流中,此符号进栈;
- 若是括号,
- 遇到左括号直接进栈,
- 遇到右括号,则栈中左括号及左括号以上符号全部出栈进入输出流中;
- 一直到最后输入为空时,栈中剩余的符号全部出栈进入输出流中,此时输出流中就是后缀表达式;
[中序表达式转换为后序.png]
所以计算机处理我们标准四则运算式(中缀表达式)分为两步:
- 把中缀表达式转化为后缀表达式,栈用来进出符号、左右括号;
- 运算后缀表达式并运算得出结果,栈用来进出数字
队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。队列是一种先进先出的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。队列操作的是相同类型的数据,操作和线性表相同,有以下操作:
- initQueue()初始化队列,创建一个空队列
- destroyQueue()若队列存在,删除队列
- clearQueue()清空队列
- isEmpty()若队列为空,则返回true,若队列不为空,则返回false
- getHead()若对队列存在且非空,返回队头元素
- setTail()若队列存在,插入元素到队列中,并成为队尾元素
- deleteHead()删除队列的队头元素,并返回
- length队列元素的个数
循环队列的定义
把实现队列的顺序存储结构的头尾相接,称为循环结构。为了避免只有一个元素时队头和队尾重合使处理变得麻烦,所以引入了两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时,此时队列不是只剩一个元素,而是空队列。
那么问题来了,当空队列时,front等于rear;当队列满时,也是front等于rear,那马究竟该如何来判断此时的嘟列是空还是满呢?
- 办法一:设置一个编辑变量flag,当frontrear且flag=false时,表示此时队列为空;当frontrear且flag=true时,表示此时队列满;
- 办法二:当嘟列为空时,条件就是front==rear;当队列满时,我们保留一个元素空间;
针对办法二:我们设队列的最大长度为queueSize,那么队列满的条件为:(rear+1)%queueSize==front;
计算队列长度的通用公式为:(rear-front+queueSize)%queueSize
/*循环队列的初始化*/
void initCircularQueue(CQueue queue) {
queue.front = 0;
queue.rear = 0;
}
/*循环队列求队列长度*/
int cqLength(CQueue queue) {
return (queue.rear-queue.front+queueSize)%queueSize;
}
/*入队操作,向未满队列queue中插入数据d*/
void insertToQueue(CQueue queue, DataType d) {
if ((queue.rear+1)%queueSize==queue.front) { // 队满的判断
return ERROR;
}
queue.data[queue.rear] = d;
queue.rear = (queue.rear+1)%queueSize;
}
/*出队操作,删除队头数据并返回*/
DatsType deleteFromQueue(CQueue queue, DataType d) {
if (queue.front == queue.rear) { // 队列为空的判断
return ERROR;
}
d = queue.data[queue.front]; // 接收队头数据并返回
queue.front = (queue.front+1)%queueSize; // 队头指针后移一位,若到最后则转到数组头部
return d;
}
队列的链式存储结构及其实现
队列的链式存储结构其实就是线性表的单链表,只能尾进头出,简称链队列;
/*链队列的入队操作,其实就是在链表的尾部插入结点*/
void insertToLinkedQueue(LinkedQueue queue, DataType d) {
LinkedQueueNode s = new LinkedQueueNode(); // 创建新结点
if (s==null) { // 分配内存失败,退出
Syatem.exit();
}
s.data = d; // 把数据d存入新结点s
s.next = null; // 新结点s的后继指针置空
queue.rear.next = s; // queue尾结点的后继指针指向新结点s
queue.rear = s; // queue的尾指针指向新结点s
}
/*链队列的出队操作,其实就是头结点的后继结点出队*/
DataType deleteFromLinkedQueue(LinkedQueue queue, DataType d) {
if (queue.front == queue.rear) {
return ERROR;
}
LinkedQueueNode p = new LinkedQueueNode();
p = queue.front.next; // 把要删的队头结点(即链表头结点的后继结点)暂存在结点p
d = p.data; // 接收结点p数据,并返回
queue.front.next = p.next;
if (queue.rear == p) {
queue.rear = queue.front; // 如果队头和队尾重合,则删除后把rear指向头结点
}
/*free(p)释放结点p*/
return d;
}
第五章 字符串
有零个或多个字符组成的有限序列。一般即为 s = “abcdefghi”,注意双引号不属于字符串的内容值,组成字符串的字符可以是数字、大小写字母、中文字符、特殊符号或者其他字符。字符串的长度是指组成字符串的字符的个数,零个字符的字符串称为空串。所谓有限序列是指组成字符串的字符的个数是有限个,序列,说明字符串的相邻字符之间具有前驱和后继的关系。
空格串有一个或多个空格组成,是有长度的。字符串中由任意个连续字符所组成的子序列称为该字符串的子串,该串称为子串的主串。
字符串的基本操作:
- StrAssign(T, chars) 生成一个值等于字符串常量chars的字符串T;
- StrCopy(T, S) 字符串S存在,由S复制得到字符串T;
- clear(T) 字符串S存在,将其清空;
- isEmpty(T) 判断字符串T是否为空,若为空返回true,否则返回false;
- length(T) 返回字符串的长度,即字符串包含字符元素的个数;
- compare(S, T) 若S>T返回值>0;若S=T返回值=0;若S<T返回值<0;
- concat(T, S1, S2) 用T返回有S1和S2拼接而成的新字符串;
- subString(sub, S, pos, len)若字符串S存在,1<=pos<=length(S),且0<=len<=length(S)+1-pos,用sub返回字符串S的第pos个字符之后长度为len的子串;
- index(S, T, pos) 若字符串S和T存在,T是非空串,1<=pos<=length(S),若主串S中存在和字符串T值相同的子串,则返回这个子串在主串S中,第pos个字符之后第一次出现的位置,否则返回0;
- replace(S, T, V) 若字符串S,T,V存在且T为非空字符串,用V替换主字符串S中出现的所有与T相等的不重叠子串;
- insert(S, pos, T) 若字符串S,T存在,1<=pos<=length(S)+1,在字符串S的第pos个字符前面插入字符串T;
- delete(S, pos, len) 若字符串S存在,1<=pos<=length(S)-len+1,在字符串S删除第pos个字符起,长度为len的子串;
子串的定位操作index,借助字符串基本操作实现
/**
*
*
* 字符串定位操作index的实现:
*/
int index(String s, String t, int pos) {
private String sub = null;
if (pos>0) {
int n = length(s);
int m = length(t);
int i = pos;
while (i<=n-m+1) {
subString(sub, s, i, m); // 取主串弟弟i个位置,长度与t相等的子串,用sub返回
if (compare(sub, t) != 0) { //如果两串不相等
++i;
} else {
return i; // 如果找到有子串与t相等,返回位置
}
}
}
return 0; // 如果没有子串与t相等,则返回0
}
字符串的存储结构分两种:顺序存储结构、链式存储结构
- 顺序存储结构:用一组地址连续的存储单元来存储字符串中的字符序列;一般用定长数组为每个预定义大小的字符串变量分配一个固定长度的存储区;一般把实际的串长度值保存在数组0下标位置;
- 链式存储结构:一个结点存储的字符数,需要根据实际情况做出选择;
子串的定位操作index,普通数组实现,时间复杂度O((n-m+1)*m)
/**
* 用基本的数组实现字符串的模式匹配算法index
* 字符串s和t的长度分别记录在s[0]和t[0]
*/
int index(String s, String t, int pos) {
int i = pos; // i记录主串s中当前位置下标,若pos不为1,则从pos位置开始匹配
int j = 1; // j记录子串t中当前位置下标
while (i <= s[0] && j <= t[0]) { // 若i小于s长度且j小于t长度时,开始循环计较
if (s[i] == t[j]) { // 两个字符相等,则i,j同时后移一位继续比较
++i;
++j;
} else { // 指针后退重新开始比较
i = i - j + 2; // i退回带上次匹配首位的下一位
j = 1; // j退回到子串t的首位
}
}
if (j > t[0]) {
return i - t[0];
} else {
return 0;
}
}
子串的定位操作index,KMP匹配算法实现,时间复杂度O(n+m)
/**
* 返回目标字符串t的next数组NextArr
*/
void getNextArr(String t) {
private int i = 1;
private int j = 0;
private int[] nextArr = new int[];
nextArr[1] = 0;
while (i < t[0]) { // 此处t[0]表示字符串t的长度
if (j == 0 || t[i] == t[j]) { // t[i]表示后缀子串的单个字符,t[j]表示前缀子串的单个字符
++i;
++j;
nextArr[i] = j;
} else {
j = nextArr[j];
}
}
}
/**
* index算法,返回目标子串t在主串s中第pos个字符之后的位置。若不存在则返回0
* t非空且1<=pos<=length(s)
*/
int indexKMP(String s, String t, int pos) {
private int i = pos; // i用于记录主串s当前位置的下标,若pos不为1则从pos位置开始匹配
private int j = 1; // j用于记录子串t当前字符的下标位置
private int[] next = new int[];
getNextArr(t, nxt); // 对子串t作分析,得到next数组
while (i<=s[0] && j<=t[0]) { // i<=主串s的长度,j<=子串t的长度时,继续循环
if (j==0 || s[i]==t[j]) { // 两字符相等则继续,相较于普速算法增加了j=0的判断
++i;
++j;
} else { // 指针后退,重新开始匹配
j = next[j]; // j退回到合适的位置,i值不变
}
}
if (j>t[0]) {
return i-t[0];
} else {
return 0;
}
}
此部分有待补充KMP算法的深入理解,小猿第一次看完KMP算法只能算是一知半解,还需要下次深入研究!