队列和栈--链表,数组的实现

一,队列

队列的逻辑含义:

队列(Queue)是一种先进先出(FIFO, First In First Out)的数据结构,意味着队列中的元素按照它们进入队列的顺序依次处理。即,最先被添加到队列中的元素最先被移除。

队列的基本操作:
  1. 入队(Enqueue):将一个元素添加到队列的末尾。
  2. 出队(Dequeue):从队列的头部移除一个元素。
  3. 查看队头元素(Front/Peek):获取队列头部的元素,但不移除它。
  4. 判断队列是否为空(IsEmpty):检查队列中是否有元素。
  5. 获取队列的大小(Size):返回队列中元素的个数。

队列的物理实现:

队列在计算机中的实现可以使用多种数据结构,最常见的物理实现有以下几种:

1. 数组实现
  • 队头队尾都通过数组索引来表示。
  • 入队操作将元素放到队尾,出队操作将队头元素移除。

优点

  • 操作简单,直接使用数组即可。

缺点

  • 空间浪费:当出队时,队头元素被移除,但数组的空间并没有被回收,可能导致空间浪费。
  • 队列长度限制:如果数组大小固定,队列的最大长度也是固定的,可能会遇到溢出问题。

解决方案
使用一个指针(或两个指针)来追踪队头和队尾,并且当队头移动时,可以实现一个循环队列(Circular Queue)。这样就可以避免空间浪费。

循环队列:队头和队尾在数组的两端循环,解决了数组中空间浪费的问题。具体做法是通过模运算来实现“首尾相接”:

 
  

front = (front + 1) % capacity; rear = (rear + 1) % capacity;

2. 链表实现
  • 链表队列通常使用单链表来实现。
  • 队头指向链表的第一个节点,队尾指向链表的最后一个节点。
  • 入队:通过将新元素添加到链表的尾部来实现。
  • 出队:通过移除链表的头部元素来实现。

优点

  • 动态分配内存,大小不固定。
  • 不存在空间浪费的问题。

缺点

  • 每次入队时,需要找到链表的尾部(如果没有尾指针的话),这可能导致较高的时间复杂度(O(n))。
  • 实现稍复杂,涉及指针操作。

优化:使用尾指针来直接指向队尾,这样入队操作就可以在 O(1) 时间内完成。

3. 双端队列(Deque)
  • 双端队列是允许从两端进行插入和删除的队列。
  • 它既支持队列的先进先出(FIFO)特性,也支持从队头和队尾都能进行操作

优点

  • 既能实现队列的操作,也能实现栈的操作。

缺点

  • 实现更为复杂。

队列的物理实现示意:

  1. 数组实现(普通队列)

     

    队列头 → [元素1, 元素2, 元素3, ..., 元素N] ← 队列尾

    在数组队列中,通常使用两个指针:

    • front:指向队列的头部,出队时移动。
    • rear:指向队列的尾部,入队时移动。

    如果采用循环数组实现,那么当 front 和 rear 都指向数组的末尾时,它们可以“绕回”到数组的开始部分,从而避免空间浪费。

  2. 链表实现

     

    队列头 → [元素1] → [元素2] → [元素3] → ... → [元素N] → 队列尾

    链表队列的关键是使用两个指针:

    • front:指向链表的第一个元素。
    • rear:指向链表的最后一个元素。

    在入队操作时,rear 移动到链表的末尾,新的节点连接到它上面;在出队操作时,front 移动到下一个节点。

总结:

  • 逻辑:队列是一种先进先出(FIFO)的数据结构,具有入队、出队、查看队头、检查队列空否等基本操作。
  • 物理实现
    • 使用数组实现时,队列的大小固定,可能存在空间浪费问题。可以通过循环数组来优化。
    • 使用链表实现时,队列的大小动态,且不容易产生空间浪费,但操作时需要涉及指针管理。
    • **双端队列(Deque)**支持从两端插入和删除。

队列在实际应用中广泛用于任务调度、数据缓冲、打印队列等场景。

二,栈

栈的逻辑含义

栈(Stack)是一种后进先出(LIFO, Last In First Out)的数据结构。栈中的元素只有一个端口进行插入和删除操作,这个端口被称为栈顶(Top)。栈遵循的主要原则是:最新插入的元素最先被删除。常见的栈操作包括:

  1. 压栈(Push):将一个元素放入栈顶。
  2. 弹栈(Pop):从栈顶移除一个元素。
  3. 查看栈顶元素(Top/Peek):查看栈顶元素,但不移除它。
  4. 检查栈是否为空(IsEmpty):检查栈中是否有元素。
  5. 栈的大小(Size):返回栈中元素的数量。

栈通常用于需要追踪"最近"或"递归"的场景,比如:浏览器的历史记录、表达式求值、函数调用的管理等。

栈的物理实现

栈的物理实现可以通过数组链表两种方式。

1. 数组实现

在数组实现中,栈的元素存储在一个连续的内存块中(即数组)。栈顶由一个指针或索引(例如 top)指向,指示当前栈顶的位置。

  • 入栈(Push):将一个新元素放置到数组中索引为 top+1 的位置,然后将 top 指针递增。
  • 出栈(Pop):将栈顶的元素移除,并将 top 指针递减。

数组实现的优缺点

  • 优点
    • 操作简单,直接通过数组索引即可访问栈元素。
    • 入栈和出栈的时间复杂度都是 O(1)。
  • 缺点
    • 栈的大小是固定的,如果栈满了就无法继续入栈。
    • 需要预先分配足够的内存空间来存储栈中的元素,如果栈空间用尽,需要重新分配内存(动态扩展)。

代码示例

#include 
using namespace std;

class Stack {
private:
    int* arr;
    int top;
    int capacity;

public:
    Stack(int size) {
        capacity = size;
        arr = new int[size];
        top = -1;  // 栈为空时,top为-1
    }

    bool isEmpty() {
        return top == -1;
    }

    bool isFull() {
        return top == capacity - 1;
    }

    void push(int x) {
        if (isFull()) {
            cout << "Stack Overflow" << endl;
            return;
        }
        arr[++top] = x;
    }

    void pop() {
        if (isEmpty()) {
            cout << "Stack Underflow" << endl;
            return;
        }
        top--;
    }

    int peek() {
        if (!isEmpty()) {
            return arr[top];
        }
        cout << "Stack is Empty" << endl;
        return -1;
    }

    ~Stack() {
        delete[] arr;
    }
};
2. 链表实现

在链表实现中,栈的元素以链表节点的形式存在,栈顶由一个指针指向链表的头节点。每个新元素都被插入到链表的头部,形成栈顶。

  • 入栈(Push):创建一个新节点,将其指向当前的栈顶,然后更新栈顶指针指向新节点。
  • 出栈(Pop):移除栈顶节点,并将栈顶指针指向下一个节点。

链表实现的优缺点

  • 优点
    • 栈的大小是动态的,可以根据需要增加或减少内存使用。
    • 不会发生栈溢出问题,除非系统内存不足。
  • 缺点
    • 每次操作都需要通过指针管理,稍微复杂。
    • 每次出栈时需要销毁节点,可能涉及内存管理问题(如内存泄漏)。

代码示例

#include 
using namespace std;

class Stack {
private:
    struct Node {
        int data;
        Node* next;
    };
    Node* top;

public:
    Stack() {
        top = nullptr;
    }

    bool isEmpty() {
        return top == nullptr;
    }

    void push(int x) {
        Node* newNode = new Node;
        newNode->data = x;
        newNode->next = top;
        top = newNode;
    }

    void pop() {
        if (isEmpty()) {
            cout << "Stack Underflow" << endl;
            return;
        }
        Node* temp = top;
        top = top->next;
        delete temp;
    }

    int peek() {
        if (!isEmpty()) {
            return top->data;
        }
        cout << "Stack is Empty" << endl;
        return -1;
    }

    ~Stack() {
        while (!isEmpty()) {
            pop();
        }
    }
};

栈的物理实现总结

  • 数组实现:栈的大小固定,操作简单,但可能会遇到栈满的情况,需要手动扩展。
  • 链表实现:栈的大小动态,可以更好地适应内存的使用,但需要通过指针来管理节点,稍显复杂。

栈的应用场景

  1. 递归的模拟
    栈是递归的基础。每次递归调用都将函数的返回地址、局部变量等信息压入栈中,并在函数执行完毕后弹出栈顶的内容。栈模拟了递归函数的调用和返回过程。

  2. 表达式求值
    栈被广泛用于运算符的优先级处理和表达式的求值,尤其是在中缀表达式转换为后缀表达式(或前缀表达式)时。

  3. 深度优先搜索(DFS)
    在图的遍历中,DFS 使用栈来追踪当前的路径,并回溯到上一层进行其他未探索的路径。

  4. 浏览器历史记录
    浏览器的前进和后退按钮通常依赖栈来存储用户的浏览历史。

  5. 撤销操作
    文本编辑器中的撤销功能通常会使用栈来记录操作历史,用户可以逐步回退到先前的状态。

总结

栈是一种**后进先出(LIFO)**的线性数据结构,通过数组或链表实现,具有简单高效的操作(O(1)时间复杂度)。它广泛应用于递归、表达式求值、图遍历等问题中。

三,队列的实现

一,链表

1,误区:指针与引用,类的成员变量。

1. 基本概念:

在 C++ 中,函数的参数传递可以是两种方式:

  • 值传递(Pass by value):将变量的副本传递给函数,函数内对副本的修改不会影响函数外的原始变量。
  • 引用传递(Pass by reference):将变量的地址(即指针)传递给函数,函数内对这个引用的修改会直接影响外部的原始变量。

2. 指针与引用的区别:

在你提供的代码中,tail 是一个指针类型的成员变量。当你在 insert() 函数内修改 tail 时,你实际上是通过直接修改这个指针指向的内存位置,而不是修改 tail 变量本身。关键点是:

  • tail 是类的成员变量,因此它是一个指针类型的成员
  • 当你通过 tail->next = p 更新 tail 指向的新节点时,你实际上是改变了指针 tail 的内容。
  • 因为 tail 是类的成员变量,它的修改会直接反映在类的实例(对象)上,因此即使 tail 是在 insert() 函数中修改的,函数外的 tail 也会受到影响。

3. 为什么修改 tail 会影响函数外的 tail

  • tail 是指向队列尾部的指针,它是类的成员变量,而不是局部变量。
  • 当你在 insert() 中使用 tail = p; 时,你并没有创建一个局部变量,而是直接修改了这个成员变量 tail
  • 类的成员变量是与类的实例(对象)相关联的,而不是局部作用域内的临时变量。因此,在类方法内部修改 tail 会直接影响到该对象的 tail 成员。

4. 示意说明:

假设有以下的队列类:

class Queue {
public:
    Node* head;
    Node* tail;

    Queue() {
        head = nullptr;
        tail = nullptr;
    }

    void insert(int x) {
        Node* p = new Node;
        p->value = x;
        p->next = nullptr;

        if (tail == nullptr) {  // 如果队列为空
            head = tail = p;
        } else {
            tail->next = p;  // 将原 tail 的 next 指向新节点
            tail = p;        // 更新 tail 为新节点
        }
    }
};

在调用 insert() 时,tail 的修改直接影响了该对象的成员变量 tail,因为:

  • tail 是类的成员指针,指向队列尾部。
  • 当你修改 tail = p;,你并没有修改局部变量,而是修改了对象的成员变量。因此,无论函数结束与否,tail 在对象中的值都会发生变化

5,类成员变量

类成员变量可以在类的成员函数中直接使用,而无需作为参数传入。这是面向对象编程中的基本特性之一。类成员变量(通常称为“实例变量”)是类的一部分,它们属于对象的状态,所有的类成员函数都可以直接访问和修改这些变量。

原因:

  1. 类成员变量是对象的一部分:当你创建一个类的实例时,类的成员变量就成为该实例的一部分,每个对象都可以独立持有这些成员变量。

  2. 类成员函数内部可以访问类的成员变量:类的成员函数(方法)是与对象绑定的,并且它们在对象实例上执行,因此它们可以直接访问和修改该实例的成员变量,无需通过参数传递。

  3. 隐式访问机制:在类成员函数内部,你可以直接引用类成员变量,因为它们是当前对象的一部分,编译器会自动知道当前对象的上下文。

6. 总结:

修改类的成员变量时,函数内对该成员变量的修改会影响函数外的对象,因为成员变量是与对象实例相关联的,而不是局部作用域内的临时变量。因此,在 insert() 函数中修改 tail(以及 head)会直接影响到队列实例的状态。

这种行为与指针传递引用传递的原理密切相关。通过指针操作类的成员变量,函数内的修改会反映到对象的实际成员中。

class Queue
{
	
public:
	Node* head = NULL;
	Node* tail = head;


	void insert(int x)
	{
		Node* p = new Node;
		p->value = x;

		if (head == NULL && tail == NULL)
		{
			head = p;
		}
		else
		{
			tail->next = p;
			tail = tail->next;
		}
		
	}

	int pop()
	{
		Node* temp;
		int result;

		temp = head;
		result = head->value;
		head = head->next;
		delete temp;
		return result;
	}
};

二,数组

1,原理:通过更新左右索引来实现插入弹出操作。

2,代码:

#include

using namespace std;

class Queue
{
public:
	int l = 0, r = 0;
	int* arr;
	int size;
	
	//create the an array 
	Queue(int n)
	{
		size = n;
		arr = new int[n];
	}

	void insert(int n)
	{
		if (r < size)arr[r++] = n;
		else printf("the queue is full !");		
	}

	int pop()
	{
		
		if (l < r) return arr[l++];
		else
			printf("the queue is empty !");
	}

	int peak()
	{
		return arr[r];
	}
};


int main()
{
	Queue queue (5);
	for (int i = 0; i < 6; i++)
	{
		queue.insert(i);
		cout << queue.pop() << " ";
	}

	return 0;
}

三,栈的数组实现

代码展示:

#include

using namespace std;

class Stack
{
public:
	int* arr;
	int size = 0;
	int index = 0;

	Stack(int n)
	{
		arr = new int[n];
		size = n;
	}

	void insert(int x)
	{
		if (index == size)
		{
			printf("the stack is full !\n");
			return;
		}
		arr[index++] = x;
		return;
	}

	int pop()
	{
		if (index < 0)
		{
			printf("the stack is empty !");
			return -1; 
		}
		int result = arr[index - 1];
		index--;

		return result;
	}
};

int main()
{
	Stack stack(2);
	stack.insert(5);
	stack.insert(6);
	stack.insert(7);
	cout << stack.pop();
	return 0;

}

注意点:

1,对于有返回值的函数,要使它的所有情况都有返回值,即使是错误情况,也要,例如pop函数。

你可能感兴趣的:(链表,数据结构)