列表、栈和队列

列表

列表是一个有限的、有序的数据项序列,其中的每一项被称为元素。

  • 每个元素在列表中都有一个位置。
  • 每个元素可以是任意类型,但所有元素的类型相同。
  • 列表的长度是当前存储的元素数量
  • 空列表不包含任何元素。
  • 列表的开头和结尾分别称为头(head)和尾(tail)。

支持的操作:

  • 插入
  • 追加
  • 删除
  • 查找
  • 判空
  • 上一个元素
  • 下一个元素
  • 当前位置
  • 移动到指定位置
  • 移动到开头
  • 获取长度等

实现

函数定义

// List ADT
template <typename E> class List {
public:
List() {}
virtual ~List() {}
virtual bool insert(const E& item) = 0; //插入
virtual bool append(const E& item) = 0; //拼接到尾部
virtual bool remove(E&) = 0; //删除
virtual void clear() = 0; //清空
virtual void moveToStart() = 0; //移动到开头
virtual void moveToEnd() = 0; //移动到结尾
virtual void prev() = 0; //前驱
virtual void next() = 0; //后继
virtual int currPos() const =0; //当前位置
virtual void moveToPos(int pos) = 0; //移动到指定位置
……
}

一般有两种实现方法:一种基于数组,一种基于指针。

数组实现

基本思想:预分配一个大小为 MAX_SIZE 的大数组,使用变量计数来跟踪当前大小,使用变量来跟踪当前位置。当需要插入或删除元素时移动元素。

运行效率:这里考虑 N 个元素的情况:平均而言,必须移动一半的元素才能腾出空间。假设在位置插入的可能性相同,最坏的情况是在位置 0 插入。必须在插入之前将所有 N 个项目移动一个位置,所以这是 O(N) 运行时间。最好的情况下是O(1)

以下是伪代码实现:

// insert element at the current position
void insert(const E& it) {
Assert(listSize>=maxSize, “Exceed capacity”);
//shift Elements up
for(int i=listSize; i>curr; i--)
listArray[i] = listArray[i-1];
listArray[curr] = it; // insert the element
listSize++; // increment list size
}

// Remove and return the current element
E remove() {
Assert((curr>=0)&&(curr<listSize), “no element);
E it = listArray[curr]; // Copy the element
for(int i=curr; i<listSize-1; i++)
// Shift them down
listArray[i] = listArray[i+1];
listSize--; // Decrement size
return it;
}

bool moveToPos(int pos) {
Assert((pos>=0)&&(pos<listSize)), “out of range);
curr = pos;
}
void moveToStart() { curr = 0; } //reset position
void moveToEnd() { curr = listSize; } //set at end
void prev() { if(curr!=0) curr--; }
void next() { if(curr < listSize) curr++; }
int Length() const { return listSize; }
int currPos() const { return curr; }

//Return true if K is in list, otherwise,false
bool find(List<int>& L, int K) {
int it;
for(L.moveToStart(); L.currPos()<L.Length(); L.next()) {
it = getValue(); //return value of curr element
if (K == it) return true; // Found it
}
return false; // Not found
}

在C++的STL库中提供了vector,可以动态扩容。

链表实现

特征

  • 使用动态内存分配,根据需要为新列表元素分配内存。
  • 元素称为节点,它们使用指针链接。
  • 通过将节点链接在一起来跟踪列表。
  • 当您想要插入或删除时更改链接。

节点实现

// An implementation of a simple
// singly-linked list node
template <typename E> class Link {
public:
E element; //value for this node
Link *next; //Pointer to next node in list
//Constructors
Link(const E& elemval, Link* nextval =NULL)
{ element = elemval; next = nextval; }
Link(Link* nextval =NULL)
{ next = nextval; }
};

关键变量

  • 头指针head – 用于扫描整个列表
  • 尾指针rear – 加速“追加”操作
  • Curr 指针 – 指向当前元素
  • cnt – 存储列表的长度

//Class LList
//inherit the abstract class List
template <typename E> class LList: public List<E>{
private:
Link<E>* head; //Pointer to list header
Link<E>* tail; //Pointer to last element
Link<E>* curr; //access to current element
int cnt; //Size of list
void init() //used by constructor
{
curr = tail = head = new Link<E>;
cnt =0;
}
void removeall() //used by deconstructor
{
while(head != NULL) {
curr=head;
head=head->next;
delete curr;
}
}
}

操作

插入

  • 创建一个新的链表节点,存储新元素
  • 设置新节点的下一个字段
  • 设置 curr 指向的节点的下一个字段

//Insert a node to current position
public:
void insert(const E& it) {
curr->next = new Link<E>(it, curr->next);
//先创建新节点指向12,再将cur的下一个元素指向新节点
if (tail == curr) tail = curr->next; //new tail
//如果是在尾部插入,那么需要更新tail
cnt++;
}

//Append a node at the tail of the list
void append(const E& it) {
tail = tail->next = new Link<E>(it, NULL);
cnt++;
}

删除

  • 删除节点只需要重定向要删除的节点周围的一些指针即可。
  • 记得回收被删除节点占用的空间

// Remove and return current element
E remove() {
E it = curr->next->element;
Link<E> *ltemp = curr->next;
if (tail == curr->next) //删除的恰好是尾部元素
tail =curr; // Reset tail
curr->next = curr->next->next; //remove element
delete ltemp; //reclaim space
cnt--; //decrement the list size
return it;
}

后继

//Next – move curr one pos toward the tail
void next() { // no change if already at end
if (curr != tail) {
curr = curr->next;
}
}

前驱

//Prev – move curr one pos toward the head
void prev() {
if (curr == head) return; // No previous element
Link<E>* temp = head;
//march down list until the previous element
while (temp->next!=curr) temp=temp->next;
curr = temp;
}

比较

linked lists are more space efficient when implementing lists whose number of elements varies widely or is unknown.

链表在以下情况下更节省空间:实现其元素数量的列表变化很大或未知。

Array-based lists are generally more space efficient when the user knows in advance approximately how large the list will become.

基于数组的列表通常需要更多空间。

空间花费比较

指针实现:

  • 当前列表中元素的数量 - n
  • 指针的大小 – P
  • 数据元素的大小 – E
  • 数组中元素的最大数量 – D

数组实现:基于数组的列表需要空间 D*E

链表需要空间:n*(P+E)

\(n > \frac{D*E}{P+E}\),基于数组实现的列表空间效率更高。

练习:确定链表比基于数组的列表更有效的盈亏平衡点

  1. 数据字段为2字节,指针为4字节,数组有30个元素:n < DE/(P+E) = 2*30/(2+4) = 10.
  2. 数据字段8字节,指针4字节,数组有30个元素:n < DE/(P+E) = 8*30/(8+4) = 20
  3. 数据字段为 32 个字节,指针为 4 个字节,数组有 40 个元素:n < DE/(P+E) = 32*40/(32+4) = 35.555

栈是一种运行插入和删除的列表:仅在列表的一端(顶部)执行操作,特征是先进先出。

主要有以下操作:

  • Push:在顶部插入元素
  • Pop:删除并返回顶部元素
  • IsEmpty:测试是否为空

还是有两种实现:数组实现和链表实现。

// The stack ADT
template <typename E> class Stack {
private:
void operator=(const Stack&) {} //Protect assignment
Stack(const Stack&) {} //Protect copy assignment
public:
// Push an element onto the top of the stack.
virtual bool push(const E& it) = 0;
// Remove the element at the top of the stack.
virtual E pop() = 0;
// Return a copy of the top element
virtual const E& topValue() const = 0;
};

数组实现

  • 栈必须声明为固定的大小
emplate <typename E> class Astack: public Stack<E> {
private:
int maxSize; // Maximum size of stack
int top; // Index for top element (free position)
E *listArray; // Array holding stack elements
public:
AStack(int size=defaultSize) //Constructor
{ maxSize=size; top=0; listArray=new E[size]; }
~Astack() { delete [] listArray; } //Destructor
  • 数组尾部元素作为栈顶:

    通过将元素追加到列表尾部来将其压入堆栈,每次成本都是\(O(1)\)

    顶部设置

    • 堆栈中第一个空闲位置的数组索引,空栈的顶部设置是0。

    • Push:先插入元素,然后递增top

    • Pop:先递减top,然后移除top元素;

      注意两个操作的顺序。

代码实现:

void clear() { 
top=0;
} //Reinitialize

void push(const E& it) { // put “it” on stack
Assert(top != maxSize, “Stack is full”);
listArray[top++] = it;
}

E pop() {//pop top element
Assert(top != 0, “Stack is empty”);
return listArray[--top];
}

const E& topValue() const { //return top element
Assert(top != 0, “Stack is empty”);
return listArray[top-1];
}

链表实现

注意:元素仅从列表头部插入和删除

//A linked stack
template <typename E> class LStack: public Stack<E> {
private:
Link<E>* top; //pointer to first element
int size; //number of elements
public:
LStack(int sz = defaultSize){ //Constructor
top = NULL; size = 0;
}
~LStack() {
clear();
} //Destructor

代码实现:

void clear() { //reinitialize
while (top != NULL) { //delete link nodes
Link<E>* temp = top; top = top->next; delete temp;
}
size = 0;
}

void push(const E& it) { //put “it” on the stack
top = new Link<E>(it, top); size++;
}

E pop() { //remove “it” from stack
Assert(top != NULL, “Stack is empty”);
E it = top->element;
Link<E>* ltemp = top->next;
delete top; top = ltemp; size--; return it;
}

const E& topValue() const { //return top value
Assert(top != 0, “Stack is empty”);
return top->element;
}
  • 不需要有头节点:零个或一个元素的列表不需要特殊代码。
  • 指针top指向第一个链接节点:
    • Push:首先修改新创建节点的next字段指向栈顶,然后设置top指向新节点
    • Pop:设置top指向旧顶节点的下一个链接;旧的顶部节点被释放并返回其元素值。

队列

队列只允许元素从列表的一端(后面)插入,从列表的另一端(前面)删除。即先进先出。

入队:在后面插入一个元素

出队:从前面删除一个元素

template <typename E> class Queue {
private:
void operator =(const Queue&) {}
Queue(const Queue&) {};
public:
Queue() {}
virtual ~Queue() {}
virtual void clear() = 0;
virtual void enqueue(const E&) = 0;
virtual E dequeue() = 0;
virtual const E& frontValue() const = 0;
virtual int length() const = 0;
}

数组实现

//Array Implementation for Queue
template <typename E> class AQueue: public Queue<E> {
private:
int maxSize; // Maximum size of queue
int front; // Index of front element
int rear; // Index of back element
E *listArray; // Array holding queue elements
int currSize; //length of queue

};

假设在数组的前n个位置插入n个元素:

如果选择位置0作为队列的头部,出队时间花费为\(\Theta(n)\),入队时间花费为\(\Theta(1)\)

如果选择数组中的n-1作为队列的头部,出队需要花费\(\Theta(1)\),入队需要花费\(\Theta(n)\)

需要连续的地址空间,并且队列可以在数组内进行移动(平移漂流)

漂流队列

  • 队列的前端最初位于数组的位置 0
  • 将元素添加到连续编号较高的位置
  • 当元素被移除时,前面的索引会增加
  • 入队和出队都花费 \(θ(1)\) 时间

但是这可能会导致数组未满时,队列已满。为了解决这个问题,引入了循环队列。

循环队列

使用取模运算实现,位置maxSize-1在位置0之前。

  • front=rear时,队列中只有一个元素。

  • front比rear`大1时,队列是空还是满?

    • 解决方案1:显式地记录队列中元素的数量

    • 解决方案2:使数组大小为n+1,并且只允许有n个元素

      • front=rear+1,队列为空
      • front=rear+2,队列已满。

      解释一下:因为只能容纳n个元素,所以必定会多一个空位置。对应的也就有front=rear+2,容纳了n个元素,如果是front=rear+1,当队列为空时,frontrear 指向相同的位置(即队列的起点),这表示没有元素可供读取。因为队列是循环的,"相同位置"的判断可以通过取模运算来实现:front == (rear + 1) % capacity。这种情况下,队列没有元素,所以我们定义它为空。

//Array-based Implementation (solution 2)
template <typename E> class AQueue: public Queue<E> {
private:
int maxSize; // Maximum size of queue
int front; // Index of front element
int rear; // Index of rear element
E *listArray; //Array holding queue elements
public:
AQueue(int size =defaultSize) {
// Constructor- Make list array one position
// larger for empty slot
maxSize = size+1;
rear = 0; front = 1;
listArray = new E[maxSize];
}
~AQueue() {
delete [] listArray;
}

//reinitialize
void clear() {rear = 0; front = 1;}
void enqueue(const E& it) {//put “it” in queue
Assert(((rear+2) % maxSize) != front,“Queue is full”);
rear = (rear+1)%maxSize;
listArray[rear] = it;
}

E dequeue() {//take element out
Assert(length() != 0, “Queue is empty”);
E it = listArray[front];
front =(front+1)%maxSize;
return it;
}
//get front value
const E& frontValue() const {
Assert(length()!=0, “Queue is empty”);
return listArray[front];
}

//return length
virtual int length() const {
return ((rear+maxSize)-front+1)%maxSize;
}

链表实现

结构

  • 使用头节点
  • front指针始终指向头节点
  • 后指针指向队列中最后一个链接节点

操作

  • 入队:将新元素放入链表末尾的链接节点中,向后移动指向新插入的节点
  • 出队:移除并返回列表的第一个元素

比较

  • 时间成本:两种实现的所有成员函数都需要恒定时间 \(θ(1)\)

  • 空间成本:对于基于数组的队列,如果队列未满,会造成一些空间浪费。对于链接队列,每个元素都有链接字段的开销