前篇讨论了一下具体的链表结构以及常规操作。现在我们趁热打铁,感觉再来熟悉一下具体的操作。此时我们用链表的形式来实现之前的顺序栈。
链栈
栈结构这里不多说,我们直接进入正题。先考虑,当栈为空的时候,如何表示?当然就是一个空指针了
将新元素推入堆栈时,该元素只会添加到链表的前端。因此,如果将元素e1 push到空栈上,该元素将存储在一个新单元格中,该单元格将成为链中唯一的链接:
将一个新元素push栈将该元素添加到链的开头。所涉及的步骤与将字符插入链表列表缓冲区所需的步骤相同。 首先分配一个新的单元格,然后输入数据,最后更新链接指针,使新单元格成为链中的第一个元素。 因此,如果将元素e2推到栈上,将得到以下配置:
在链表表示中,pop操作包括删除链中的第一个单元格并返回存储在该列中的值。因此,来自上图所示的栈的pop操作返回e2并恢复栈的先前状态,如下所示:
对链栈中的pop和push的分析
push
我们以一个最基本的数字栈为基础分析,下面是需要的一些接口内容:
class StackInt { // in StackInt.h
public:
StackInt (); // constructor
void push(int value); // append a value
int pop(); // return the first-in value
private:
struct Node {
int value;
Node * link;
};
Node * data; // member variables
};
接下来看代码分析:
void StackInt::push(int v) {
Node * temp = new Node;
temp->value = v;
temp->link = data; //这一步必不可少!
data = temp;
}
老办法,图解push过程,假设此时我们的链栈如下:
我们的目的是push(7),将7放入栈顶,也就是要这样的结果:
下面解释整个过程:
- 新建一个临时节点,并用创建临时指针指向该节点,接着向该节点写入数字7,就像这样:
- 然后,将头指针data里面的指针值,赋值给新节点的link字段,以便新节点可以连接到8这个节点:
- 将temp里面的值,赋值给首指针data处。将data的指向正确改变:
- 函数返回,得出正确的push后的结构:
pop
先看代码:
int StackInt::pop() {
int toReturn = data->value;
Node * temp = data;
data = temp->link;
delete temp;
return toReturn;
}
图解过程如下:
- 先将要pop出去的值进行保存
- 我们这边创建一个临时的指针变量Temp,然后将其指向首指针data指向的节点(即栈顶元素),就像这样:
- 然后改变首指针(即栈顶)的指向:
- 最后删除temp指向的节点
- 此时函数返回。
注意,我们第二步不可以直接改变指针的指向而不使用临时节点,因为直接改变指针的指向会像下图一样:
这显然不是我们希望看到的。
链队列
队列类也使用链表结构进行简单的表示。为了说明基本的方法,队列的元素存储在从队列头部开始并以尾部结尾的列表中。并且为了保证入队和出队正确运行,Queue对象必须含有指向队列两端的指针。 因此,私有实例变量的定义在queuepriv.h的修改版本如下:
/*
*这个文件保存的是Queue.h的私有部分
*我们这个时候用链队列。也就是用链表来实现我们的队列
*这样的队列我们称为链队列
*/
private:
/*队列的链式结构*/
struct Cell{
ValueType data;
Cell *link;
};
/*实例化变量*/
Cell *head; //头指针
Cell *tail; //尾指针
int count; //记录队列元素的总数
/* Make it illegal to copy queues */
Queue(const Queue & value) { }
const Queue & operator=(const Queue & rhs) { return *this; }
那么为什么我们要设置一个尾指针呢?
首先我们假设一下我们初始化的队列如下所示:
这个时候我们执行后入队操作:
显然,这个时候我们相当于在链表中插入一个元素,且位于头结点的第一个,自然我们的复杂度为O(1)。但是当我们执行出队操作时,情况就不同了,我们必须验证头指针遍历到最后一个节点,再执行delet操作,此时算法复杂度为O(N)。
入队(enqueue)
那么是否有更好的办法呢?因此我们考虑一下添加一个尾指针(因为队列多数在链表的两端操作)。所以我们看下面一段代码:
void QueueInt::enqueue(int v) {
Node * temp = new Node;
temp->value = v;
tail->link = temp;
tail = temp;
}
图解代码执行过程:
- 假设我们初始的队列值为123,并且我们添加的尾指针指向的是我们的3的节点,如图:
- 此时我们在队列尾部(3),这里加入数值4,此时,我们的操作应该是,建立一个指针,指向新建立的节点,然后往这个新节点写入数值:
- 接下来我们改变指针的指向,也就是将cp的内容复制到3下面的link字段(也就是tail指向的节点的link字段)。此时情况如下(分两步):
- 最终我们返回我们的队列就可以了,因此复杂度为O(1)
出队(dequeue)
我们就拿上面完成的例子为例,进行我们的dequeue操作,我们先看2 3行代码,建立一个临时节点,指向与我们的head指针的指向相同,如下:
首先假设我们有完整的队列
现在我们要进行的queue操作,于是将队头移除,以达到下图的结果:
同理,看代码:
int QueueInt::dequeue() {
int toReturn = head->value;
Node * temp = head;
head = temp->link;
delete temp;
return toReturn;
}
- 我们先将此时队头指向的数字给存储起来,然后新建一个指针,跟队头指针一同指向首元素1.
- 然后改变头指针的指向
- 释放被dequeue掉的那个节点