栈的定义

(stack)是一种线性数据结构,它就像一个上图所示的放入乒乓球的圆筒容器,栈中的元素只能先入后出(First In Last Out,简称FILO)。最早进入的元素存放的位置叫作栈底 (bottom),最后进入的元素存放的位置叫作栈顶 (top)。

通常,栈的开口端被称为栈顶;相应地,封口端被称为栈底。因此,栈顶元素指的就是距离栈顶最近的元素,拿下图来说,栈顶元素为元素4;同理,栈底元素指的是位于栈最底部的元素,下图中的栈底元素为元素1。

在顺序栈中用数组data[0..MaxSize-1​]存放栈中元素,其中一端作为栈顶,另一端作为栈顶。我们的通常做法是讲data[0]作为栈底,data[MaxSize-1]做为栈顶。对顺序栈SqStack的声明是:

typedef struct{
    ElmeType data[MaxSize];  //存放栈中的数据元素
    int top;  //栈顶指针,栈顶元素在data数组中的下标
}SqStack;

判断栈空的条件:s->top == -1;

判断栈满的条件:s->top == MaxSize - 1;

元素e的进栈操作:先将栈顶指针top增1,然后将元素e放在栈顶指针处;

出栈栈顶元素:先将栈顶指针top处的元素取出放在e中,然后将栈顶指针减1。


栈的基本操作
  • 初始化栈 InitStack(&s)
  • 销毁栈 DestoryStack(&s)
  • 判断是否为空栈 StackEmpty(s)
  • 取栈顶元素 GetTop(s,&e)
入栈操作

入栈操作(push)就是把新元素放入栈中,只允许从栈顶一侧放入元素,新元素的位置将会成为新的栈顶。

//进栈操作的实现 以顺序栈为例
bool Push(SqStack *&s, ElemType e)
{
    if (s->top == MaxSize - 1) //栈已满
        return flase;
    s->top++;            //栈顶指针加一
    s->data[s->top] = e; //将元素e放在栈顶指针处
    return true;
}

一个序列从1到n依次入栈, 那么可能的出栈序列一共有多少种? 注意: 在任意一个时刻,只要栈不为空, 就可能有元素出栈, 不是说元素全部入栈之后再出栈

这种条件下对应的出栈情况求和公式h(n)=C(2n,n)/(n+1)

出栈操作

出栈操作(pop)就是把元素从栈中弹出,只有栈顶元素才允许出栈,出栈元素的前一个元素将会成为新的栈顶。

//出栈操作的实现 以顺序栈为例
bool Pop(SqStack *&s, ElemType &e)
{
    if (s->top == -1) //栈已空
        return flase;
    e = s->data[s->top]; //取栈顶元素赋给e
    s->top--;            //栈顶指针减一
    return true;
}
例题
(顺序栈)判断一个字符串是否为对称串
bool symmetry(ElemType str[])
{
    int i;
    ElemType e;
    SqStack *st;   //创建一个顺序栈st
    InitStack(st); //初始化栈st
    for (i = 0; str[i] != '\0'; i++) //将str串中的每一个字符依次压入st栈
    {
        Push(st, str[i]);
    }
    for (i = 0; str[i] != '\0', i++)
    {
        Pop(st, e);  //将栈顶元素存到e中,并出栈
        if (str[i] ! = e)  //如果e不等于str[i]证明不是str对称串
        {
            DestoryStack(st);  //销毁栈st
            return false;   //返回false
        }
    }
    DestoryStack(st);  //销毁栈st
    return true;  //返回true
}
(链栈)判断表达式中的左右括号是否配对
bool Match(char exp[], int n)
{
    int i = 0;
    char e;
    bool match = true;  //初始化match为true
    LinkStNode *st;
    InitStack(st);  //初始化链栈st
    while (i < n && match) 
    {
        if (exp[i] == '(') //如果exp[i]为左括号
            Push(st, exp[i]);  //将exp[i]入栈st
        else if (exp[i] == ')')  //如果exp[i]为右括号
        {
            if (GetTop(st, e) == true) //判断链栈st是否有栈顶元素,若有取出将栈顶元素的值存在e中
            {
                if (e != '(')   //如果e不是为左括号
                    match = false; //match置为false
                else
                    Pop(st, e); //将e对应的栈顶元素出栈 match是true
            }
            else
                match = false;  //若栈顶没有元素,直接将match置为false
        }
        i++; // i++,回到while循环
    }
    if (!StackEmpty(st)) //while循环完成,如果st不是空栈,说明还有左括号留在栈中,没有右括号与它匹配
        match = false;  //将match置为false
    DestoryStack(st);  //销毁栈st
    return match;   //函数返回最终的match值
}

队列

队列的定义

队列(queue)是一种线性数据结构,它的特征和行驶车辆的单行隧道很相似。不同于栈的先入后出,队列中的元素只能先入先出 (First In First Out,简称FIFO )。队列的出口端叫作队头 (front),队列的入口端叫作队尾 (rear)。对于顺序队列SqQuence的声明是:

typedef struct{
    ElmeType data[MaxSize];  //存放队列中的数据元素
    int front,rear;          //队头和队尾指针
}SqQuence;

判断队空的条件:q->front == q->rear;

判断队满的条件:q->rear == MaxSize - 1;

元素e的进队操作:先将rear增1,然后将元素e放在data数组的rear位置;

出队操作:先将front增1,然后取出data数组中front位置的元素。


队列的基本操作
  • 初始化队列 InitQueue(&q)
  • 销毁队列 DestoryQueue(&q)
  • 判断是否为空队列 QueueEmpty(q)
入队操作

入队(enqueue)就是把新元素放入队列中,只允许在队尾的位置放入元素,新元素的下一个位置将会成为新的队尾。

//入队操作 顺序队
bool enQueue(SqQueue *&q, ElemType e)
{
    if (q->rear == Maxsize - 1) //队已满
        return flase;
    q->rear++;            //队尾增一
    q->data[q->rear] = e; //队尾位置插入元素e
    return true;
}
出队操作

出队操作(dequeue)就是把元素移出队列,只允许在队头一侧移出元素,出队元素的后一个元素将会成为新的队头。

//出队操作 顺序队
bool deQueue(SqQueue *&q, ElemType &e)
{
    if (q->front == q->rear) //队已空
        return flase;
    q->front++;           //队头增一
    e = q->data[q->front]; //取队头位置元素
    return true;
}
环形队列
数组形式的循环(环形)队列

假设一个队列经过反复的入队和出队操作,还剩下2个元素,在“物理”上分布于数组的末尾位置。这时又有一个新元素将要入队。

在数组不做扩容的前提下,如何让新元素入队并确定新的队尾位置呢?我们可以利用已出队元素留下的空间,让队尾指针重新指回数组的首位。

这样一来,整个队列的元素就“循环”起来了。在物理存储上,队尾的位置也可以在队头之前。当再有元素入队时,将其放入数组的首位,队尾指针继续后移即可。

一直到(队尾下标+1)%数组长度 = 队头下标 时,代表此队列真的已经满了。需要注意的是,队尾指针指向的位置永远空出1位,所以队列最大容量比数组长度小1。

环形队列基本概念

判断队空的条件:q->front == q->rear​;

判断队满的条件:(q->rear+1)%MaxSize == q->front​;

元素e的进队和出队操作:分别将队尾rear和队头front循环进1。

front = (front + 1)%MaxSize
rear = (rear + 1)%MaxSize

设一个环形队列中数组的下标是0~N-1,队头指针为f(指向队头元素的前一个位置)、队尾指针为r(指向队尾元素),则其元素个数为(r-f+N)%N​


环形队列进队和出队的代码实现
//环形队列 进队列
bool enQueue(SqQueue *&q, ElemType e)
{
    if ((q->rear + 1) % MaxSize == q->front) //队满上溢出
        return false;
    q->rear = (q->rear + 1) % MaxSize;   //队尾指针rear加一
    q->data[q->rear] = e; //元素e插入到到队尾
    return true;
}
//环形队列 出队列
bool deQueue(SqQueue *&q, ElemType &e)
{
    if (q->front == q->rear)  //队空下溢出
        return false;
    q->front = (q->front + 1) % MaxSize; //队首指针rear加一
    e = q->data[q->front]; //取出该位置元素赋值给e
    return true;
}

基础知识

串是由0个或多个字符组成的有限序列

串也具有线性存储结构链式存储结构,前者称为顺序串,后者称为链串

在顺序串上实现串的基本运算的算法

生成串、串的复制、判断串是否相等、串的连接、求子串、子串的插入、子串的删除、子串的替换、输入串

设S为一个长度为n的字符串,每个字符串各不相同,则有:

S子串个数为n(n+1)/2+1 (含有S本身和空串)

S的非空子串个数为n(n+1)/2

S的互异非空子串个数为n(n+1)/2-1


空串空格串的区别?

空串不含有任何字符,长度为0,空串是任意串的子串。空格串仅含有空格字符,其长度是串中空格字符的个数。例如:

  • 空串:存储 0 个字符的串,例如 S = ""(双引号紧挨着);
  • 空格串:只包含空格字符的串,例如 S = " "(双引号包含3个空格)

求串s中出现的第一个最长连续字符子串的下标和长度
//函数传入了一个串s,得到第一个最长连续相同字符构成的子串
void LongestString(SqString s, int &index, int &maxlen)
{
    int length, i = 1, start; //length保存子串的长度
    index = 0;                //index表示最终找到的子串在s中开始的位置
    maxlen = 1;               //maxlen保存其长度
    while (i < s.length)      //当i小于s的长度
    {
        start = i - 1;                           //用start表示临时子串开始的位置,下标要减一
        length = 1;                              //先将子串长度置为1
        while (i < s.length && s.data[i] == s.data[i - 1]) //当i小于s的长度且当前位置的值等于前一个位置的值
        {
            i++;      //表示前后的字符重复 i自增1
            length++; //长度加一
        }
        if (maxlen < length) //上面的while结束后,判断如果length大于maxlen
        {
            maxlen = length; //将maxlen更新
            index = start;   //将子串开始的下标更新
        }
        i++; //i自增1,回到最外层while循环
    }
}
串的模式匹配

串的模式匹配算法,通俗地理解,是一种用来判断两个串之间是否具有"主串与子串"关系的算法。若两串匹配,子串在主串中的位置,指的是子串首个字符在主串中的位置。

BF算法(暴力算法)

BF算法的平均时间复杂度是O(m*n),BF 算法的实现过程很"无脑",不包含任何技巧,在对数据量大的串进行模式匹配时,算法的效率很低,所以要对其进行改进。

代码实现

//传入主串s和子串t,若匹配成功返回子串t在主串s中的位置,若不匹配,返回-1
int BF(SqString s, SqString t)
{
    int i = 0, j = 0;  //i用于记录主串s当前下标的位置 j用于记录子串t当前下标的位置
    while (i < s.length && j < t.length) //遍历子串和主串
    {
        if (s.data[i] == t.data[j]) //如果子串和主串的对应的字符相同
        {
            i++;   //主串下标后移一位
            j++;   //子串下标后移一位
        }
        else  //若发现对应字符不相同
        {
            i = i - j + 1; //主串的当前位置i前进一位
            j = 0;         //子串的当前位置j回到初始位置
        }
    }
    if (j >= t.length)  //匹配成功
        return (i - t.lenth); //返回子串在主串中的位置
    else  //匹配失败
        return (-1); //返回-1
}
KMP算法

KMP算法是改进的串的模式匹配算法,KMP算法的平均时间复杂度为O(m+n),KMP算法还可以继续改进。

代码实现

//返回子串t的next数组
void Get_Next(SqString t, int next[])
{
    int j, k;
    j = 0;   //用j扫描t
    k = -1;  //用k记录t[j]之前与t开头相同的字符个数
    next[0] = -1; //设置next[0]的值为-1
    while (j < t.length-1 ) //求t所有位置的next值
    {
        if (k == -1 || t.data[j] == t.data[k]) //t.data[j]表示后缀的单个字符,t.data[k]表示前缀的单个字符
        {
            j++; //j后移一位
            k++; //k后移一位
            next[j] = k; //并设置next[j]的值为k
        }
        else
            k = next[k]; //若字符值不相同且k!=-1,将k值回溯
    }
}
//返回子串t在主串s中的位置,若不匹配,返回-1
int KMPIndex(SqString s, SqString t)
{
    int i = 0; //i用于记录主串s当前下标的位置
    int j = 0;   //j用于记录子串t当前下标的位置
    int next[MAXSIZE]; //初始化一个next数组
    Get_Next(t, next); //调用函数Get_Next得到子串t的next数组
    while (i < s.length && j < t.length) //i和j都没有越界时继续循环
    {
        if (j == -1 || s.data[i] == t.data[j]) //当j=-1或s.data[i]与t.data[j]相匹配时,将子串和主串当前下标位置同时后移一位
        {
            i++;
            j++;
        }
        else  //否则就将j回溯
        {
            j = next[j]; 
        }
    }
    if (j >= t.length)  //如果最后j的值大于等于模式串的值则匹配成功,返回子串位置
        return (i - t.length);
    else  //否则匹配失败 返回-1
        return (-1);
}
改进的KMP算法

改进的KMP只不过是把next[ ]数组改进成nextval[ ]数组,在代码中多了一个判断:如果后移一位后的t.data[j] = t.data[k]就设置nextval[j]的值为nextval[k],其余情况下与普通的next[ ]数组的求法一致,对应的KMP算法是一样的思路,改进后的KMP算法的平均时间复杂度还是O(m+n)

下面是求子串nextval数组的代码

//返回子串t的nextval数组
void Get_Nextval(SqString t, int nextval[])
{
    int j, k;
    j = 0;               //用j扫描t
    k = -1;              //用k记录t[j]之前与t开头相同的字符个数
    nextval[0] = -1;     //设置nextval[0]的值为-1
    while (j < t.length) //求t所有位置的nextval值
    {
        if (k == -1 || t.data[j] == t.data[k]) //t.data[j]表示后缀的单个字符,t.data[k]表示前缀的单个字符
        {
            j++;                        //j后移一位
            k++;                        //k后移一位
            if (t.data[j] != t.data[k]) //判断后移一位的字符是否相同,若不相同设置nextval[j]的值为k
            {
                nextval[j] = k;
            }
            else
                nextval[j] = nextval[k]; //若相同设置nextval[j]的值为nextval[k]
        }
        else
            k = nextval[k]; //若字符值不相同且k!=-1,将k值回溯
    }
}