数据结构与算法 Chapter 3 受限线性表

3.1 栈

栈(stack)又称堆栈,是一种被限定仅在表尾进行插入和删除的线性表。能进行插入和删除的一端成为栈顶(top),另一端称为栈底(bottom)。利用栈顶指针记录当前的栈顶位置。在栈顶插入称作入栈操作,在栈顶删除称作出栈操作
栈遵循“后进先出”(LIFO)“先进后出"(FILO)原则。栈的基本操作包括创建栈、销毁栈、入栈、出栈、获得栈顶元素、栈的长度、判断栈是否为空等。

3.2 栈的存储结构

因为栈是一种特殊的线性表,因此同第二章介绍的线性表存储结构类似,栈也有顺序存储方式与链式存储方式。

3.2.1 顺序栈

栈的顺序存储结构称作顺序栈(sequence stack),在高级语言中利用一位数组实现。用base指针记录该数组的起始位置,下标变量top记录当前栈顶的下一个元素。当数组已满时进行入栈操作,栈会发生上溢;类似地,当数组为空时进行出栈操作,栈会发生下溢。下面的代码定义了顺序栈类并实现了顺序栈的有关操作,并给出了一个使用顺序栈管理数据的例子:

#include <iostream>
using namespace std;

template<class Type>
class SeqStack
{
public:
    SeqStack(int size);
    ~SeqStack();
    int IsEmpty() const {return top == 0;}
    int IsFull() const {return top == maxsize;}
    void SeqStackClear() {top = 0;}
    int SeqStackLength() {return top;}
    bool Push(Type e);
    bool Pop(Type & e);
    Type GetPop();
    void Print() const;
private:
    int maxsize;
    Type * base;
    int top;
};

template<class Type>
SeqStack<Type>::SeqStack(int size) : maxsize(size)
{
    base = new Type[maxsize];
    top = 0;
}

template<class Type>
SeqStack<Type>::~SeqStack()
{
    delete [] base;
}

template<class Type>
bool SeqStack<Type>::Push(Type e)
{
    if (IsFull()) return false;
    base[top++] = e;
    return true;
}

template<class Type>
bool SeqStack<Type>::Pop(Type & e)
{
    if (IsEmpty()) return false;
    e = base[--top];
    return true;
}

template<class Type>
Type SeqStack<Type>::GetPop()
{
    if (IsEmpty()) return NULL;
    else return base[top - 1];
}

template<class Type>
void SeqStack<Type>::Print() const
{
    for (int i = 0; i < top; i++)
        cout << base[i] << " ";
    cout << endl;
}

int main()
{
    SeqStack<int> a(20);
    int b;
    a.Push(2);
    a.Push(5);
    a.Push(7);
    a.Print();
    a.Push(8);
    a.Pop(b);
    a.Pop(b);
    a.Print();
    a.SeqStackClear();
    a.Push(2);
    cout << a.SeqStackLength() << endl;
}

同线性表的顺序存储结构一样,顺序栈在使用过程中需要预先指定数组大小,因此可能会出现空间不足而难以扩张的问题。为此提出了双向栈,即将两个不同的栈的栈底指针分别置于一个大数组的两端,两个栈在入栈的过程中栈顶指针向中间的方向移动。在此种情况下,当top1==top2时,两个栈公用的存储空间已满。

3.2.2 链栈

栈的链式存储结构称作链栈(linked stack)。若将单链表的删除与插入操作限制在头结点进行,则成为一个链栈,此时单链表的head指针即变成链栈的top指针。一般而言倾向于使用无头结点的链栈。下面的代码定义了链栈类,实现了链栈上的操作并且给出了一个用链栈管理数据的简单例子:

#include <iostream>
using namespace std;

template<class Type> class LinkStack;
template<class Type> class LinkStackNode
{
    friend class LinkStack<Type>;
public:
    LinkStackNode(Type & e, LinkStackNode<Type> * p = NULL) : elem(e), next(p) {}
private:
    Type elem;
    LinkStackNode<Type> * next;
};
template<class Type> class LinkStack
{
public:
    LinkStack() : top(NULL) {}
    ~LinkStack()
    {
        LinkStackNode<Type> * p = top;
        while (top)
        {
            p = top;
            top = top->next;
            delete p;
        }
    }
    int isEmpty() const {return top == NULL;}
    void LinkStackClear()
    {
        LinkStackNode<Type> * p = top;
        while (top)
        {
            p = top;
            top = top->next;
            delete p;
        }
        top = NULL;
    }
    int LinkStackLength() const
    {
        int count = 0;
        LinkStackNode<Type> * p = top;
        while (p)
        {
            count++;
            p = p->next;
        }
        return count;
    }
    Type GetTop()
    {
        if (isEmpty()) return NULL;
        else return top->elem;
    }
    void Push(Type e)
    {
        LinkStackNode<Type> * p = new LinkStackNode<Type>(e);
        p->next = top;
        top = p;
    }
    Type Pop()
    {
        if (isEmpty()) return NULL;
        LinkStackNode<Type> * p = top;
        Type q = p->elem;
        top = top->next;
        delete p;
        return q;
    }
    void Print()
    {
        LinkStackNode<Type> * p = top;
        while (p)
        {
            cout << p->elem << " ";
            p = p->next;
        }
        cout << endl;
    }
private:
    LinkStackNode<Type> * top;
};

int main()
{
    LinkStack<int> a;
    int b;
    a.Push(2);
    a.Push(5);
    a.Push(7);
    a.Print();
    a.Push(8);
    a.Pop();
    a.Pop();
    a.Print();
    a.LinkStackClear();
    a.Push(2);
    cout << a.LinkStackLength() << endl;
    return 0;
}

值得注意的是,在链栈的入栈、出栈、获取栈顶元素的操作中,应首先检查栈是否处于满或空的状态,再对应地执行操作或返回错误信息。

3.3 栈的应用

应用一:括号配对
用户输入一串由“[({])}”组成的括号字符串,可以通过栈的方式来检测其中的括号是否一一配对。当下一个字符是左括号时,将该左括号入栈;当下一个字符是右括号时,若该右括号与当前栈顶的括号匹配,则对栈执行出栈操作,否则说明括号不匹配。处理完整个字符串后的栈应当为空,若不为空也说明不匹配。换言之,仅当能够成功处理完字符串并且处理完后栈为空时说明该括号字符串中的括号是一一配对的。下面的代码由用户输入一个括号字符串,程序将返回该括号字符串的匹配情况以及不匹配的具体原因:

#include <iostream>
using namespace std;

template<class Type> class LinkStack;
template<class Type> class LinkStackNode
{
    friend class LinkStack<Type>;
public:
    LinkStackNode(Type & e, LinkStackNode<Type> * p = NULL) : elem(e), next(p) {}
private:
    Type elem;
    LinkStackNode<Type> * next;
};
template<class Type> class LinkStack
{
public:
    LinkStack() : top(NULL) {}
    ~LinkStack()
    {
        LinkStackNode<Type> * p = top;
        while (top)
        {
            p = top;
            top = top->next;
            delete p;
        }
    }
    int isEmpty() const {return top == NULL;}
    void LinkStackClear()
    {
        LinkStackNode<Type> * p = top;
        while (top)
        {
            p = top;
            top = top->next;
            delete p;
        }
        top = NULL;
    }
    int LinkStackLength() const
    {
        int count = 0;
        LinkStackNode<Type> * p = top;
        while (p)
        {
            count++;
            p = p->next;
        }
        return count;
    }
    Type GetTop()
    {
        if (isEmpty()) return NULL;
        else return top->elem;
    }
    void Push(Type e)
    {
        LinkStackNode<Type> * p = new LinkStackNode<Type>(e);
        p->next = top;
        top = p;
    }
    Type Pop()
    {
        if (isEmpty()) return NULL;
        LinkStackNode<Type> * p = top;
        Type q = p->elem;
        top = top->next;
        delete p;
        return q;
    }
    void Print()
    {
        LinkStackNode<Type> * p = top;
        while (p)
        {
            cout << p->elem << " ";
            p = p->next;
        }
        cout << endl;
    }
private:
    LinkStackNode<Type> * top;
};

int getID(char c)
{
    if (c == '(' || c == ')') return 1;
    if (c == '[' || c == ']') return 2;
    if (c == '{' || c == '}') return 3;
}

int main()
{
    string s;
    cin >> s;
    LinkStack<char> st;
    int i = 0;
    for (i = 0; i < s.size(); i++)
    {
        if (s[i] == '(' || s[i] == '[' || s[i] == '{') st.Push(s[i]);
        else
        {
            if (getID(s[i]) == getID(st.GetTop())) st.Pop();
            else break;
        }
    }
    if (i == s.size() && st.isEmpty()) cout << "Match" << endl;
    else if (i == s.size()) cout << "Missing right parentheses" << endl;
    else if (st.isEmpty()) cout << "Missing left parentheses" << endl;
    else cout << "Parentheses mismatch" << endl;
    return 0;
}

值得注意的是,在解决这个括号配对问题之中,我们所使用的是栈的ADT,因此无论采用顺序栈或者链栈的物理存储结构并不会影响问题的求解与结果。

应用二:递归
递归是一种重要的程序设计方法,意为函数直接或间接地调用本身,而函数的递归可以用栈来实现。当一个函数被调用时,在栈顶分配为其分配一块存储空间;当一个函数返回时,从栈顶删除其存储空间。汉诺塔问题是一个经典的递归问题(在这里省略代码实现)。

3.4 队列

队列是限定只能在一端进行插入操作而在另一端进行删除操作的线性表。插入操作的称作队尾,删除操作的称作队列头。队列符合“先进先出”(FIFO)的特征。队列的基本操作包括创建队列、销毁队列、入队、出队、获得队列头元素、队列的长度、判断队列是否为空等。

3.5 队列的存储结构

队列是一种操作受限的线性表,因此与线性表一样具有顺序与链式存储结构。

3.5.1 顺序队列

顺序队列以一个地址连续的数组做存储空间,使用下标变量front标记队列头的元素位置,下标变量rear标记队列尾的下一个元素的位置。但是在这样实现的队列中会出现假溢出现象,即rear == maxsize-1front != 0,因此front之前仍有未利用的空闲空间。为了防止队列出现假溢出现象,采取循环队列,在每一次出队或入队后更新下标变量的值时,采取rear = (rear+1) % maxsizefront = (front + 1) % maxsize的方式,从而使得数组下标0的位置接在数组下标maxsize-1位置之后。
在这种情况下,空队列判别:rear == front,而满队列判别:(rear + 1) % maxsize == front,因此在队列满时其实仍有一个空,但为了与空的情况产生区隔,在这里故意留出一个空来。也可以使用状态变量来记录当前队列内的元素个数,这样就不会有这个一个空的问题了。
下面给出代码定义顺序队列的存储结构,实现队列的基本操作并用一个简单例子展示队列上数据的操作:

#include <iostream>
using namespace std;

template<class Type>
class SeqQueue
{
private:
    int front, rear;
    Type * queue;
    int maxsize;
public:
    SeqQueue(int size) : maxsize(size + 1)
    {
        queue = new Type[size + 1];
        front = rear = 0;
    }
    ~SeqQueue() {delete [] queue;}
    bool isEmpty() const {return front == rear;}
    bool isFull() const {return (rear + 1) % maxsize == front;}
    void SeqQueueClear() {front = rear = 0;}
    int SeqQueueLength() {return (rear - front + maxsize) % maxsize;}
    bool InQueue(Type e)
    {
        if (isFull()) return false;
        queue[rear] = e;
        rear = (rear + 1) % maxsize;
    }
    Type OutQueue()
    {
        if (isEmpty()) return false;
        Type q = queue[front];
        front = (front + 1) % maxsize;
        return q;
    }
    Type GetFront()
    {
        if(isEmpty()) return false;
        return queue[front];
    }
    void Print() const
    {
        int p = rear;
        int q = front;
        while (q != p)
        {
            cout << queue[q] << " ";
            q = (q + 1) % maxsize;
        }
        cout << endl;
    }
};

int main()
{
    SeqQueue<int> a(20);
    a.InQueue(2);
    a.InQueue(8);
    a.InQueue(12);
    a.OutQueue();
    a.Print();
    a.SeqQueueClear();
    a.InQueue(2);
    a.InQueue(8);
    cout << a.SeqQueueLength() << endl;
    cout << a.GetFront() << endl;
    return 0;
}

注意,满队列时队列长度与元素个数不一定相等。若采取留空的方法,则队列长度比元素个数大1;若采取状态变量的方法解决假溢出,则队列长度与元素个数相等。上面给出的代码采用留空的方法解决假溢出的问题。

3.5.2 链队列

队列的链式存储结构称为链队列。利用指针front来记录队列的头结点位置,利用指针rear来记录队列的尾结点位置。链队列的入队与出队操作有一定的特殊性:在入队时,若队列本身为空,应当同时初始化frontrear指针,即front = rear = xxNode;若队列不为空,则在rear后面接上一个新的结点即可。在出队时,若队列为空则报错,不为空则将front = front->next;并且释放内存、返回元素值;在出队之后,若此时的队列为空,应当将rear也设置为NULL 。这两点对空队列的特殊处理保证了链队列在空的状态下执行操作不会出错。
下面的代码定义了链队列的物理存储结构,实现了队列的基本操作并且用一个简单例子展示如何使用链队列操作数据:

#include <iostream>
using namespace std;

template<class Type>
class LinkQueue;

template<class Type>
class LinkQueueNode
{
    friend class LinkQueue<Type>;
public:
    LinkQueueNode(Type & e, LinkQueueNode<Type> * p = NULL) : elem(e), next(p) {}
private:
    Type elem;
    LinkQueueNode<Type> * next;
};

template<class Type>
class LinkQueue
{
public:
    LinkQueue() : front(NULL), rear(NULL) {}
    ~LinkQueue()
    {
        LinkQueueNode<Type> * p = front;
        while (front)
        {
            p = front;
            front = front->next;
            delete p;
        }
    }
    bool isEmpty() const {return front == NULL;}
    void LinkQueueClear()
    {
        LinkQueueNode<Type> * p = front;
        while (front)
        {
            p = front;
            front = front->next;
            delete p;
        }
        front = rear = NULL;
    }
    int LinkQueueLength() const
    {
        LinkQueueNode<Type> * p = front;
        int count = 0;
        while (p)
        {
            count++;
            p = p->next;
        }
        return count;
    }
    Type GetFront()
    {
        if (isEmpty()) return NULL;
        return front->elem;
    }
    void InQueue(Type e)
    {
        if (front == NULL) // deal with empty queue
            front = rear = new LinkQueueNode<Type>(e,NULL);
        else
            rear = rear->next = new LinkQueueNode<Type>(e,NULL);
    }
    Type OutQueue()
    {
        if (isEmpty()) return NULL;
        LinkQueueNode<Type> * p = front;
        Type q = p->elem;
        front = front->next;
        delete p;
        if(front == NULL) rear = NULL;
        return q;
    }
    void Print()
    {
        LinkQueueNode<Type> * p = front;
        while (p)
        {
            cout << p->elem << " ";
            p = p->next;
        }
        cout << endl;
    }
private:
    LinkQueueNode<Type> * front;
    LinkQueueNode<Type> * rear;
};

int main()
{
    LinkQueue<int> a;
    a.InQueue(2);
    a.InQueue(8);
    a.InQueue(12);
    a.OutQueue();
    a.Print();
    a.LinkQueueClear();
    a.InQueue(2);
    a.InQueue(8);
    cout << a.LinkQueueLength() << endl;
    cout << a.GetFront() << endl;
    return 0;
}

再次强调一下在链队列中,入队操作与出队操作应当特殊处理空的队列,保证其rearfront指针的正确性。

3.6 队列应用

这里举一个杨辉三角的顺序队列解法,下面的代码输入一个n后输出n阶的杨辉三角,核心是利用杨辉三角元素等于两肩上元素之和的特性,同时利用队列的性质实现:

#include <iostream>
using namespace std;

template<class Type>
class SeqQueue
{
private:
    int front, rear;
    Type * queue;
    int maxsize;
public:
    SeqQueue(int size) : maxsize(size + 1)
    {
        queue = new Type[size + 1];
        front = rear = 0;
    }
    ~SeqQueue() {delete [] queue;}
    bool isEmpty() const {return front == rear;}
    bool isFull() const {return (rear + 1) % maxsize == front;}
    void SeqQueueClear() {front = rear = 0;}
    int SeqQueueLength() {return (rear - front + maxsize) % maxsize;}
    bool InQueue(Type e)
    {
        if (isFull()) return false;
        queue[rear] = e;
        rear = (rear + 1) % maxsize;
    }
    Type DeQueue()
    {
        if (isEmpty()) return false;
        Type q = queue[front];
        front = (front + 1) % maxsize;
        return q;
    }
    Type GetFront()
    {
        if(isEmpty()) return false;
        return queue[front];
    }
    void Print() const
    {
        int p = rear;
        int q = front;
        while (q != p)
        {
            cout << queue[q] << " ";
            q = (q + 1) % maxsize;
        }
        cout << endl;
    }
};

void YH(int n)
{
    SeqQueue<int> q(n+1);
    for (int i = 0; i < n-1; i++) // fix blank
        cout << ' ';
    cout << '1' << endl;
    q.InQueue(1);
    int s = 0; // last element
    for (int i = 2; i <= n; i++)
    {
        q.InQueue(0); // 0 on both sides
        for (int k = 0; k < n - i; k++) // fix blank
            cout << ' ';
        for (int j = 1; j <= i; j++)
        {
            int t = q.DeQueue();
            cout << s+t << " ";
            q.InQueue(s+t);
            s = t;
        }
        cout << endl;
    }
}

int main()
{
    cout << "How many layers of YH triangle would you want?" << endl;
    int n;
    cin >> n;
    YH(n);
    return 0;
}

3.7 字符串

字符串也有顺序与链式结构,一般而言使用顺序结构因为其空间利用率较高,下面重点讨论串的模式匹配,模式匹配指的是在一个字符串中寻找特定子串是否存在的过程。
方法一:Brute-Force算法
是最朴素的模式匹配算法,即在主串上依次从0,1,...开始分别将从该字符开始的n个字符与长度为n的子串相比较,若不一样则主串向右滑动1的过程。代码在这里略去不给。当主串长度为n,子串长度为m时,最好情况下该算法的渐进复杂度可以达到O(m+n),最坏情况下该算法渐进复杂度为O(mn)
方法二:KMP算法
KMP算法由Knuth Morris Pratt同时发现,解决了BF算法中每次寻找失败都需要主串回溯的缺陷。其核心思想在于若出现不匹配时,主串不必回溯,而子串只需要回溯到与主串当前前几个元素由相同模式的地方(好绕啊)。具体来讲,设主串为T子串为S,设此时主串指针在i子串指针在j。若T[i] != S[j],此时i不动,而j回到next[j]的位置,其中next[j]满足:S[0,...,next[j]-1] = S[j-next[j],...,j-1]且取所有满足条件next[j]中最大的一个。若不存在,则next[j]=0,若j=0,则next[j]=-1,-1代表着主串指针i需要往右移动一位。
在实际过程中计算子串各位置上的next[j]时,通常采取以下方法:k=-1,j=0。如果k == -1 || S[j] == S[j],则有next[j+1] = next[j] + 1,体现在代码上是next[++j] = ++k;。若不然,则k应当退回到next[k]的位置,这是因为当较大的相同子模式无法找到时,可以退而求其次从S[0,...,k]当中的最大相同子模式即next[k]开始找起。通过这样的方法使j从0循环到maxsize-1,即可求出子串中所有对应位置j上的next[j],于是可以使用KMP算法完成高效的模式匹配。
下面给出KMP算法进行模式匹配的代码,用户分行输入主串和子串,然后程序会显示从主串的第几个字符(从0开始)起出现子串,或者是无匹配模式:

#include <iostream>
#include <cstring>
using namespace std;

int main()
{
    // input
    char a[1000];
    char b[20];
    cin >> a;
    cin >> b;

    // calculating next
    int next[strlen(b)];
    next[0] = -1;
    int j = 0;
    int k = -1;
    while (j < strlen(b))
    {
        if (next[j] == next[k] || k == -1)
            next[++j] = ++k;
        else
            k = next[k];
    }

    // matching
    int i = 0;
    j = 0;
    while (i < strlen(a) && j < strlen(b))
    {
        if (a[i] == b[j])
        {
            i++;
            j++;
        }
        else if(next[j] == -1)
        {
            i++;
            j = 0;
        }
        else
        {
            j = next[j];
        }
    }

    // result
    if (j == strlen(b)) cout << i - strlen(b) << endl;
    else cout << "Not found" << endl;
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/fsbblogs/p/10500601.html