当前位置: 首页 > news >正文

《算法导论》第 10 章 - 基本数据结构

        大家好!今天我们来深入学习《算法导论》第 10 章的内容 —— 基本数据结构。这一章介绍了计算机科学中最基础也最常用的数据结构,包括栈、队列、链表、树等。这些结构是构建更复杂算法和系统的基石,掌握它们对于编程能力的提升至关重要。

        本文不仅会讲解理论知识,还会提供完整可编译运行的 C++ 代码,每个知识点都配有综合应用案例,方便大家动手实践。话不多说,让我们开始吧!

思维导图

10.1 栈和队列

栈和队列是两种最基本的动态集合,它们的操作受到限制,是 "受限的线性表"

10.1.1 栈(Stack)

        栈是一种遵循后进先出(LIFO, Last In First Out) 原则的线性数据结构。想象一摞盘子,最后放上去的盘子总是最先被拿走,这就是栈的工作方式。

栈的基本操作:
  • push(S, x):将元素 x 压入栈 S
  • pop(S):移除并返回栈 S 的栈顶元素
  • top(S):返回栈 S 的栈顶元素(不删除)
  • isEmpty(S):判断栈 S 是否为空
栈的实现(C++ 代码):

我们可以用数组或链表来实现栈,这里展示数组实现:

#include <iostream>
#include <stdexcept> // 用于异常处理using namespace std;const int MAX_SIZE = 100; // 栈的最大容量class Stack {
private:int data[MAX_SIZE]; // 存储栈元素的数组int topIndex;       // 栈顶指针(索引)public:// 构造函数:初始化空栈Stack() : topIndex(-1) {}// 判断栈是否为空bool isEmpty() const {return topIndex == -1;}// 判断栈是否已满bool isFull() const {return topIndex == MAX_SIZE - 1;}// 入栈操作void push(int value) {if (isFull()) {throw runtime_error("栈已满,无法入栈");}data[++topIndex] = value; // 先移动指针,再存入元素}// 出栈操作int pop() {if (isEmpty()) {throw runtime_error("栈为空,无法出栈");}return data[topIndex--]; // 先返回元素,再移动指针}// 获取栈顶元素int top() const {if (isEmpty()) {throw runtime_error("栈为空,无法获取栈顶元素");}return data[topIndex];}// 获取栈的大小int size() const {return topIndex + 1;}// 打印栈中所有元素(从栈底到栈顶)void print() const {if (isEmpty()) {cout << "栈为空" << endl;return;}cout << "栈元素(栈底到栈顶):";for (int i = 0; i <= topIndex; ++i) {cout << data[i] << " ";}cout << endl;}
};// 测试栈的功能
int main() {try {Stack stack;// 入栈操作stack.push(10);stack.push(20);stack.push(30);stack.print(); // 应输出:栈元素(栈底到栈顶):10 20 30// 获取栈顶元素cout << "栈顶元素:" << stack.top() << endl; // 应输出:30// 出栈操作cout << "出栈元素:" << stack.pop() << endl; // 应输出:30stack.print(); // 应输出:栈元素(栈底到栈顶):10 20// 栈的大小cout << "栈的大小:" << stack.size() << endl; // 应输出:2// 继续出栈直到为空stack.pop();stack.pop();stack.print(); // 应输出:栈为空// 尝试对空栈执行出栈操作(会抛出异常)stack.pop();} catch (const exception& e) {cout << "错误:" << e.what() << endl; // 应捕获"栈为空,无法出栈"异常}return 0;
}
栈操作流程图:

10.1.2 队列(Queue)

        队列是一种遵循先进先出(FIFO, First In First Out) 原则的线性数据结构。就像排队买票,先到的人先买到票,这就是队列的工作方式。

队列的基本操作:
  • enqueue(Q, x):将元素 x 加入队列 Q 的队尾
  • dequeue(Q):移除并返回队列 Q 的队头元素
  • front(Q):返回队列 Q 的队头元素(不删除)
  • isEmpty(Q):判断队列 Q 是否为空
队列的实现(C++ 代码):

队列可以用数组或链表实现,这里展示循环数组实现(解决普通数组实现的 "假溢出" 问题):

#include <iostream>
#include <stdexcept>using namespace std;const int MAX_SIZE = 100; // 队列的最大容量class Queue {
private:int data[MAX_SIZE]; // 存储队列元素的数组int frontIndex;     // 队头指针(索引)int rearIndex;      // 队尾指针(索引)int count;          // 队列中元素的数量public:// 构造函数:初始化空队列Queue() : frontIndex(0), rearIndex(0), count(0) {}// 判断队列是否为空bool isEmpty() const {return count == 0;}// 判断队列是否已满bool isFull() const {return count == MAX_SIZE;}// 入队操作:将元素添加到队尾void enqueue(int value) {if (isFull()) {throw runtime_error("队列已满,无法入队");}data[rearIndex] = value;          // 将元素存入队尾rearIndex = (rearIndex + 1) % MAX_SIZE; // 循环移动队尾指针count++;                          // 元素数量加1}// 出队操作:移除并返回队头元素int dequeue() {if (isEmpty()) {throw runtime_error("队列为空,无法出队");}int frontValue = data[frontIndex]; // 保存队头元素frontIndex = (frontIndex + 1) % MAX_SIZE; // 循环移动队头指针count--;                          // 元素数量减1return frontValue;}// 获取队头元素(不删除)int front() const {if (isEmpty()) {throw runtime_error("队列为空,无法获取队头元素");}return data[frontIndex];}// 获取队列的大小int size() const {return count;}// 打印队列中所有元素(从队头到队尾)void print() const {if (isEmpty()) {cout << "队列为空" << endl;return;}cout << "队列元素(队头到队尾):";int index = frontIndex;for (int i = 0; i < count; ++i) {cout << data[index] << " ";index = (index + 1) % MAX_SIZE;}cout << endl;}
};// 测试队列的功能
int main() {try {Queue queue;// 入队操作queue.enqueue(10);queue.enqueue(20);queue.enqueue(30);queue.print(); // 应输出:队列元素(队头到队尾):10 20 30// 获取队头元素cout << "队头元素:" << queue.front() << endl; // 应输出:10// 出队操作cout << "出队元素:" << queue.dequeue() << endl; // 应输出:10queue.print(); // 应输出:队列元素(队头到队尾):20 30// 队列的大小cout << "队列的大小:" << queue.size() << endl; // 应输出:2// 继续入队queue.enqueue(40);queue.enqueue(50);queue.print(); // 应输出:队列元素(队头到队尾):20 30 40 50// 继续出队直到为空queue.dequeue();queue.dequeue();queue.dequeue();queue.dequeue();queue.print(); // 应输出:队列为空// 尝试对空队列执行出队操作(会抛出异常)queue.dequeue();} catch (const exception& e) {cout << "错误:" << e.what() << endl; // 应捕获"队列为空,无法出队"异常}return 0;
}
队列操作流程图:

10.1.3 栈和队列的综合应用案例

案例 1:括号匹配问题(栈的应用)

括号匹配是栈的经典应用。给定一个包含括号()[]{} 的字符串,判断字符串中的括号是否匹配。

#include <iostream>
#include <string>
#include <stdexcept> // 用于异常处理using namespace std;// 栈的实现
const int MAX_SIZE = 100; // 栈的最大容量class Stack {
private:char data[MAX_SIZE]; // 存储栈元素的数组(改为char类型,因为要存储括号)int topIndex;        // 栈顶指针(索引)public:// 构造函数:初始化空栈Stack() : topIndex(-1) {}// 判断栈是否为空bool isEmpty() const {return topIndex == -1;}// 判断栈是否已满bool isFull() const {return topIndex == MAX_SIZE - 1;}// 入栈操作void push(char value) {if (isFull()) {throw runtime_error("栈已满,无法入栈");}data[++topIndex] = value; // 先移动指针,再存入元素}// 出栈操作char pop() {if (isEmpty()) {throw runtime_error("栈为空,无法出栈");}return data[topIndex--]; // 先返回元素,再移动指针}// 获取栈顶元素char top() const {if (isEmpty()) {throw runtime_error("栈为空,无法获取栈顶元素");}return data[topIndex];}// 获取栈的大小int size() const {return topIndex + 1;}
};// 判断括号是否匹配
bool isBracketMatching(const string& s) {Stack stack;for (char c : s) {// 如果是左括号,入栈if (c == '(' || c == '[' || c == '{') {stack.push(c);}// 如果是右括号else if (c == ')' || c == ']' || c == '}') {if (stack.isEmpty()) {return false; // 右括号多了}char topChar = stack.pop();// 检查是否匹配if ((c == ')' && topChar != '(') || (c == ']' && topChar != '[') || (c == '}' && topChar != '{')) {return false; // 括号类型不匹配}}}// 循环结束后栈应该为空,否则左括号多了return stack.isEmpty();
}int main() {string test1 = "()[]{}";string test2 = "([)]";string test3 = "{[]}";string test4 = "((()))";string test5 = "(()";cout << test1 << " 是否匹配:" << (isBracketMatching(test1) ? "是" : "否") << endl; // 是cout << test2 << " 是否匹配:" << (isBracketMatching(test2) ? "是" : "否") << endl; // 否cout << test3 << " 是否匹配:" << (isBracketMatching(test3) ? "是" : "否") << endl; // 是cout << test4 << " 是否匹配:" << (isBracketMatching(test4) ? "是" : "否") << endl; // 是cout << test5 << " 是否匹配:" << (isBracketMatching(test5) ? "是" : "否") << endl; // 否return 0;
}
案例 2:滑动窗口最大值(队列的应用)

给定一个整数数组和一个滑动窗口大小,找出所有滑动窗口里的最大值。

#include <iostream>
#include <vector>
#include <deque> // 双端队列,可高效地在两端进行插入和删除操作using namespace std;// 滑动窗口最大值
vector<int> maxSlidingWindow(vector<int>& nums, int k) {vector<int> result;deque<int> q; // 存储索引,队列中对应的元素是单调递减的for (int i = 0; i < nums.size(); ++i) {// 移除队列中所有小于当前元素的元素,因为它们不可能成为最大值while (!q.empty() && nums[i] >= nums[q.back()]) {q.pop_back();}// 将当前元素的索引加入队列q.push_back(i);// 移除超出窗口范围的元素(窗口左边界为i - k + 1)while (!q.empty() && q.front() <= i - k) {q.pop_front();}// 当窗口大小达到k时,开始记录最大值(队列的 front 就是当前窗口的最大值)if (i >= k - 1) {result.push_back(nums[q.front()]);}}return result;
}// 打印向量
void printVector(const vector<int>& v) {for (int num : v) {cout << num << " ";}cout << endl;
}int main() {vector<int> nums1 = {1, 3, -1, -3, 5, 3, 6, 7};int k1 = 3;vector<int> result1 = maxSlidingWindow(nums1, k1);cout << "滑动窗口最大值:";printVector(result1); // 应输出:3 3 5 5 6 7vector<int> nums2 = {1};int k2 = 1;vector<int> result2 = maxSlidingWindow(nums2, k2);cout << "滑动窗口最大值:";printVector(result2); // 应输出:1return 0;
}

10.2 链表

        链表是一种线性数据结构,它由一系列节点组成,每个节点包含数据和指向下一个(或上一个)节点的指针。与数组相比,链表的优势是可以动态分配内存,插入和删除操作不需要移动大量元素。

10.2.1 单链表(Singly Linked List)

单链表是最简单的链表形式,每个节点只包含数据和一个指向下一个节点的指针

单链表的基本操作:
  • 初始化链表
  • 插入节点(头部、尾部、指定位置)
  • 删除节点(头部、尾部、指定值或位置)
  • 查找节点
  • 遍历链表
  • 销毁链表
单链表的实现(C++ 代码):
#include <iostream>
#include <stdexcept>using namespace std;// 单链表节点结构
template <typename T>
struct Node {T data;       // 节点数据Node* next;   // 指向下一个节点的指针// 构造函数Node(T val) : data(val), next(nullptr) {}
};// 单链表类
template <typename T>
class SinglyLinkedList {
private:Node<T>* head; // 头指针,指向链表的第一个节点int size;      // 链表中节点的数量public:// 构造函数:初始化空链表SinglyLinkedList() : head(nullptr), size(0) {}// 析构函数:销毁链表,释放内存~SinglyLinkedList() {clear();}// 判断链表是否为空bool isEmpty() const {return size == 0;}// 获取链表的大小int getSize() const {return size;}// 在链表头部插入节点void insertAtHead(T val) {Node<T>* newNode = new Node<T>(val); // 创建新节点newNode->next = head;                // 新节点指向原来的头节点head = newNode;                      // 头指针指向新节点size++;                              // 节点数量加1}// 在链表尾部插入节点void insertAtTail(T val) {Node<T>* newNode = new Node<T>(val); // 创建新节点if (isEmpty()) {// 如果链表为空,新节点就是头节点head = newNode;} else {// 否则,找到最后一个节点Node<T>* current = head;while (current->next != nullptr) {current = current->next;}current->next = newNode; // 最后一个节点指向新节点}size++; // 节点数量加1}// 在指定位置插入节点(位置从0开始)void insertAtPosition(int pos, T val) {if (pos < 0 || pos > size) {throw runtime_error("插入位置无效");}if (pos == 0) {// 在头部插入insertAtHead(val);} else if (pos == size) {// 在尾部插入insertAtTail(val);} else {// 在中间位置插入Node<T>* newNode = new Node<T>(val);Node<T>* current = head;// 找到要插入位置的前一个节点for (int i = 0; i < pos - 1; ++i) {current = current->next;}newNode->next = current->next; // 新节点指向当前节点的下一个节点current->next = newNode;       // 当前节点指向新节点size++;                        // 节点数量加1}}// 删除头部节点void deleteAtHead() {if (isEmpty()) {throw runtime_error("链表为空,无法删除节点");}Node<T>* temp = head;    // 保存头节点head = head->next;       // 头指针指向第二个节点delete temp;             // 释放原来头节点的内存size--;                  // 节点数量减1}// 删除尾部节点void deleteAtTail() {if (isEmpty()) {throw runtime_error("链表为空,无法删除节点");}if (size == 1) {// 只有一个节点,直接删除头节点delete head;head = nullptr;} else {// 找到倒数第二个节点Node<T>* current = head;while (current->next->next != nullptr) {current = current->next;}delete current->next; // 释放最后一个节点的内存current->next = nullptr; // 倒数第二个节点的next设为nullptr}size--; // 节点数量减1}// 删除指定值的第一个节点void deleteByValue(T val) {if (isEmpty()) {throw runtime_error("链表为空,无法删除节点");}// 特殊情况:要删除的是头节点if (head->data == val) {deleteAtHead();return;}// 查找要删除节点的前一个节点Node<T>* current = head;while (current->next != nullptr && current->next->data != val) {current = current->next;}// 如果找到了要删除的节点if (current->next != nullptr) {Node<T>* temp = current->next; // 保存要删除的节点current->next = current->next->next; // 跳过要删除的节点delete temp; // 释放内存size--; // 节点数量减1} else {throw runtime_error("未找到要删除的值");}}// 查找指定值的节点是否存在bool search(T val) const {Node<T>* current = head;while (current != nullptr) {if (current->data == val) {return true; // 找到节点}current = current->next;}return false; // 未找到节点}// 获取指定位置的节点值(位置从0开始)T getValueAt(int pos) const {if (pos < 0 || pos >= size) {throw runtime_error("位置无效");}Node<T>* current = head;for (int i = 0; i < pos; ++i) {current = current->next;}return current->data;}// 遍历并打印链表void print() const {if (isEmpty()) {cout << "链表为空" << endl;return;}cout << "链表元素:";Node<T>* current = head;while (current != nullptr) {cout << current->data << " -> ";current = current->next;}cout << "nullptr" << endl;}// 清空链表,释放所有节点的内存void clear() {while (!isEmpty()) {deleteAtHead();}}
};// 测试单链表的功能
int main() {try {SinglyLinkedList<int> list;// 插入操作list.insertAtHead(30);list.insertAtHead(20);list.insertAtHead(10);list.print(); // 应输出:链表元素:10 -> 20 -> 30 -> nullptrlist.insertAtTail(40);list.insertAtTail(50);list.print(); // 应输出:链表元素:10 -> 20 -> 30 -> 40 -> 50 -> nullptrlist.insertAtPosition(2, 25);list.print(); // 应输出:链表元素:10 -> 20 -> 25 -> 30 -> 40 -> 50 -> nullptr// 查找操作cout << "是否包含30:" << (list.search(30) ? "是" : "否") << endl; // 是cout << "是否包含100:" << (list.search(100) ? "是" : "否") << endl; // 否// 获取指定位置的值cout << "位置3的值:" << list.getValueAt(3) << endl; // 30// 删除操作list.deleteAtHead();list.print(); // 应输出:链表元素:20 -> 25 -> 30 -> 40 -> 50 -> nullptrlist.deleteAtTail();list.print(); // 应输出:链表元素:20 -> 25 -> 30 -> 40 -> nullptrlist.deleteByValue(25);list.print(); // 应输出:链表元素:20 -> 30 -> 40 -> nullptr// 链表大小cout << "链表大小:" << list.getSize() << endl; // 3} catch (const exception& e) {cout << "错误:" << e.what() << endl;}return 0;
}
单链表插入操作流程图:

10.2.2 双链表(Doubly Linked List)

        双链表与单链表的区别在于,每个节点不仅有指向下一个节点的指针,还有一个指向前一个节点的指针。这使得双链表可以双向遍历,某些操作(如删除指定节点)更加高效。

双链表的实现(C++ 代码):
#include <iostream>
#include <stdexcept>using namespace std;// 双链表节点结构
template <typename T>
struct DNode {T data;        // 节点数据DNode* prev;   // 指向前一个节点的指针DNode* next;   // 指向下一个节点的指针// 构造函数DNode(T val) : data(val), prev(nullptr), next(nullptr) {}
};// 双链表类
template <typename T>
class DoublyLinkedList {
private:DNode<T>* head; // 头指针DNode<T>* tail; // 尾指针int size;       // 链表中节点的数量public:// 构造函数:初始化空链表DoublyLinkedList() : head(nullptr), tail(nullptr), size(0) {}// 析构函数:销毁链表,释放内存~DoublyLinkedList() {clear();}// 判断链表是否为空bool isEmpty() const {return size == 0;}// 获取链表的大小int getSize() const {return size;}// 在链表头部插入节点void insertAtHead(T val) {DNode<T>* newNode = new DNode<T>(val); // 创建新节点if (isEmpty()) {// 如果链表为空,新节点既是头节点也是尾节点head = newNode;tail = newNode;} else {newNode->next = head; // 新节点的next指向原来的头节点head->prev = newNode; // 原来头节点的prev指向新节点head = newNode;       // 头指针指向新节点}size++; // 节点数量加1}// 在链表尾部插入节点void insertAtTail(T val) {DNode<T>* newNode = new DNode<T>(val); // 创建新节点if (isEmpty()) {// 如果链表为空,新节点既是头节点也是尾节点head = newNode;tail = newNode;} else {newNode->prev = tail; // 新节点的prev指向原来的尾节点tail->next = newNode; // 原来尾节点的next指向新节点tail = newNode;       // 尾指针指向新节点}size++; // 节点数量加1}// 在指定位置插入节点(位置从0开始)void insertAtPosition(int pos, T val) {if (pos < 0 || pos > size) {throw runtime_error("插入位置无效");}if (pos == 0) {// 在头部插入insertAtHead(val);} else if (pos == size) {// 在尾部插入insertAtTail(val);} else {// 在中间位置插入DNode<T>* newNode = new DNode<T>(val);DNode<T>* current;// 优化:根据位置选择从头部还是尾部开始查找if (pos <= size / 2) {current = head;for (int i = 0; i < pos; ++i) {current = current->next;}} else {current = tail;for (int i = size - 1; i > pos; --i) {current = current->prev;}}// 插入新节点newNode->prev = current->prev;newNode->next = current;current->prev->next = newNode;current->prev = newNode;size++; // 节点数量加1}}// 删除头部节点void deleteAtHead() {if (isEmpty()) {throw runtime_error("链表为空,无法删除节点");}DNode<T>* temp = head; // 保存头节点if (size == 1) {// 只有一个节点,头指针和尾指针都设为nullptrhead = nullptr;tail = nullptr;} else {head = head->next; // 头指针指向第二个节点head->prev = nullptr; // 新头节点的prev设为nullptr}delete temp; // 释放原来头节点的内存size--;      // 节点数量减1}// 删除尾部节点void deleteAtTail() {if (isEmpty()) {throw runtime_error("链表为空,无法删除节点");}DNode<T>* temp = tail; // 保存尾节点if (size == 1) {// 只有一个节点,头指针和尾指针都设为nullptrhead = nullptr;tail = nullptr;} else {tail = tail->prev; // 尾指针指向倒数第二个节点tail->next = nullptr; // 新尾节点的next设为nullptr}delete temp; // 释放原来尾节点的内存size--;      // 节点数量减1}// 删除指定值的第一个节点void deleteByValue(T val) {if (isEmpty()) {throw runtime_error("链表为空,无法删除节点");}DNode<T>* current = head;while (current != nullptr && current->data != val) {current = current->next;}if (current == nullptr) {throw runtime_error("未找到要删除的值");}// 如果是头节点if (current == head) {deleteAtHead();}// 如果是尾节点else if (current == tail) {deleteAtTail();}// 中间节点else {current->prev->next = current->next;current->next->prev = current->prev;delete current;size--;}}// 正向遍历并打印链表void printForward() const {if (isEmpty()) {cout << "链表为空" << endl;return;}cout << "正向遍历:";DNode<T>* current = head;while (current != nullptr) {cout << current->data << " <-> ";current = current->next;}cout << "nullptr" << endl;}// 反向遍历并打印链表(双链表的优势)void printBackward() const {if (isEmpty()) {cout << "链表为空" << endl;return;}cout << "反向遍历:";DNode<T>* current = tail;while (current != nullptr) {cout << current->data << " <-> ";current = current->prev;}cout << "nullptr" << endl;}// 清空链表,释放所有节点的内存void clear() {while (!isEmpty()) {deleteAtHead();}}
};// 测试双链表的功能
int main() {try {DoublyLinkedList<int> list;// 插入操作list.insertAtHead(30);list.insertAtHead(20);list.insertAtHead(10);list.printForward();  // 应输出:正向遍历:10 <-> 20 <-> 30 <-> nullptrlist.printBackward(); // 应输出:反向遍历:30 <-> 20 <-> 10 <-> nullptrlist.insertAtTail(40);list.insertAtTail(50);list.printForward();  // 应输出:正向遍历:10 <-> 20 <-> 30 <-> 40 <-> 50 <-> nullptrlist.insertAtPosition(2, 25);list.printForward();  // 应输出:正向遍历:10 <-> 20 <-> 25 <-> 30 <-> 40 <-> 50 <-> nullptr// 删除操作list.deleteAtHead();list.printForward();  // 应输出:正向遍历:20 <-> 25 <-> 30 <-> 40 <-> 50 <-> nullptrlist.deleteAtTail();list.printForward();  // 应输出:正向遍历:20 <-> 25 <-> 30 <-> 40 <-> nullptrlist.deleteByValue(25);list.printForward();  // 应输出:正向遍历:20 <-> 30 <-> 40 <-> nullptrlist.printBackward(); // 应输出:反向遍历:40 <-> 30 <-> 20 <-> nullptr// 链表大小cout << "链表大小:" << list.getSize() << endl; // 3} catch (const exception& e) {cout << "错误:" << e.what() << endl;}return 0;
}

10.2.3 链表的综合应用案例

案例:两数相加(链表应用)

        给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。

#include <iostream>using namespace std;// 定义链表节点
struct ListNode {int val;ListNode *next;ListNode() : val(0), next(nullptr) {}ListNode(int x) : val(x), next(nullptr) {}ListNode(int x, ListNode *next) : val(x), next(next) {}
};// 创建链表(从数组)
ListNode* createList(int arr[], int n) {if (n == 0) return nullptr;ListNode* head = new ListNode(arr[0]);ListNode* current = head;for (int i = 1; i < n; ++i) {current->next = new ListNode(arr[i]);current = current->next;}return head;
}// 打印链表
void printList(ListNode* head) {ListNode* current = head;while (current != nullptr) {cout << current->val;if (current->next != nullptr) {cout << " -> ";}current = current->next;}cout << endl;
}// 两数相加
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {ListNode* dummyHead = new ListNode(0); // 哑节点,简化边界情况处理ListNode* current = dummyHead;int carry = 0; // 进位// 遍历两个链表,直到两个链表都为空且没有进位while (l1 != nullptr || l2 != nullptr || carry > 0) {int sum = carry; // 先加上进位// 加上l1当前节点的值(如果l1不为空)if (l1 != nullptr) {sum += l1->val;l1 = l1->next; // 移动到l1的下一个节点}// 加上l2当前节点的值(如果l2不为空)if (l2 != nullptr) {sum += l2->val;l2 = l2->next; // 移动到l2的下一个节点}carry = sum / 10; // 计算新的进位current->next = new ListNode(sum % 10); // 当前位的和(取模10)current = current->next; // 移动到下一个节点}ListNode* result = dummyHead->next;delete dummyHead; // 释放哑节点的内存return result;
}int main() {// 示例1:342 + 465 = 807// 链表表示:2 -> 4 -> 3 和 5 -> 6 -> 4int arr1[] = {2, 4, 3};int arr2[] = {5, 6, 4};ListNode* l1 = createList(arr1, 3);ListNode* l2 = createList(arr2, 3);cout << "l1: ";printList(l1); // 2 -> 4 -> 3cout << "l2: ";printList(l2); // 5 -> 6 -> 4ListNode* result = addTwoNumbers(l1, l2);cout << "和: ";printList(result); // 7 -> 0 -> 8// 示例2:9999999 + 9999 = 10009998// 链表表示:9->9->9->9->9->9->9 和 9->9->9->9int arr3[] = {9, 9, 9, 9, 9, 9, 9};int arr4[] = {9, 9, 9, 9};ListNode* l3 = createList(arr3, 7);ListNode* l4 = createList(arr4, 4);cout << "\nl3: ";printList(l3); // 9->9->9->9->9->9->9cout << "l4: ";printList(l4); // 9->9->9->9ListNode* result2 = addTwoNumbers(l3, l4);cout << "和: ";printList(result2); // 8->9->9->9->0->0->0->1return 0;
}

10.3 指针和对象的实现

        在许多编程语言中,指针是直接提供的,但在一些没有显式指针的语言中,我们需要模拟指针的功能。即使在有指针的语言中,了解指针的底层实现原理也有助于我们更好地理解数据结构的工作方式。

        指针本质上是内存地址,我们可以用数组的索引来模拟指针。下面我们实现一个简单的 "内存管理器",用数组模拟内存空间,用索引模拟指针。

10.3.1 指针模拟实现(C++ 代码)

#include <iostream>
#include <vector>
#include <cstring>using namespace std;// 模拟内存块的状态
enum BlockStatus {FREE,   // 空闲USED    // 已使用
};// 内存块结构
struct MemoryBlock {BlockStatus status; // 块状态int size;           // 块大小(仅对空闲块有效)char data[32];      // 数据区域(固定大小,简化实现)int next;           // 指向下一个块的"指针"(数组索引)
};// 内存管理器类
class MemoryManager {
private:vector<MemoryBlock> memory; // 模拟内存空间const int BLOCK_SIZE;       // 每个块的大小(字节)int freeList;               // 空闲块链表的头指针(索引)public:// 构造函数:初始化内存管理器MemoryManager(int totalBlocks, int blockSize) : BLOCK_SIZE(blockSize), freeList(0) {// 初始化内存块memory.resize(totalBlocks);for (int i = 0; i < totalBlocks; ++i) {memory[i].status = FREE;memory[i].size = 1; // 每个空闲块初始大小为1个块memory[i].next = i + 1; // 指向下一个块memset(memory[i].data, 0, BLOCK_SIZE); // 初始化数据区域}memory[totalBlocks - 1].next = -1; // 最后一个块的next为-1(nullptr)}// 分配内存(模拟malloc)int allocate(int numBlocks) {if (numBlocks <= 0) return -1;int prev = -1;int current = freeList;// 查找足够大的空闲块while (current != -1) {if (memory[current].size >= numBlocks) {// 找到足够大的块if (memory[current].size == numBlocks) {// 块大小正好匹配,直接分配if (prev == -1) {freeList = memory[current].next;} else {memory[prev].next = memory[current].next;}} else {// 块太大,需要分割int splitBlock = current + numBlocks;memory[splitBlock].status = FREE;memory[splitBlock].size = memory[current].size - numBlocks;memory[splitBlock].next = memory[current].next;memory[current].size = numBlocks;if (prev == -1) {freeList = splitBlock;} else {memory[prev].next = splitBlock;}}// 标记为已使用memory[current].status = USED;return current; // 返回分配的块的"指针"(索引)}// 继续查找下一个块prev = current;current = memory[current].next;}// 没有足够的内存return -1;}// 释放内存(模拟free)void deallocate(int ptr) {if (ptr < 0 || ptr >= memory.size() || memory[ptr].status != USED) {return; // 无效的指针}// 标记为空闲memory[ptr].status = FREE;// 将释放的块插入到空闲链表的头部memory[ptr].next = freeList;freeList = ptr;// 合并相邻的空闲块(可选优化)mergeFreeBlocks();}// 合并相邻的空闲块void mergeFreeBlocks() {int current = freeList;while (current != -1 && memory[current].next != -1) {int nextBlock = memory[current].next;// 检查当前块和下一个块是否相邻if (current + memory[current].size == nextBlock) {// 合并两个块memory[current].size += memory[nextBlock].size;memory[current].next = memory[nextBlock].next;} else {// 不相邻,继续下一个块current = nextBlock;}}}// 向指定内存地址写入数据bool writeData(int ptr, const char* data, int dataSize) {if (ptr < 0 || ptr >= memory.size() || memory[ptr].status != USED || dataSize > BLOCK_SIZE * memory[ptr].size) {return false; // 无效的指针或数据太大}// 写入数据int totalSize = 0;int currentBlock = ptr;while (totalSize < dataSize && currentBlock != -1) {int blockDataSize = min(BLOCK_SIZE, dataSize - totalSize);memcpy(memory[currentBlock].data, data + totalSize, blockDataSize);totalSize += blockDataSize;currentBlock++; // 下一个块(假设连续分配)}return totalSize == dataSize;}// 从指定内存地址读取数据bool readData(int ptr, char* buffer, int bufferSize) {if (ptr < 0 || ptr >= memory.size() || memory[ptr].status != USED || buffer == nullptr) {return false; // 无效的指针}// 读取数据int totalSize = 0;int currentBlock = ptr;int maxReadSize = BLOCK_SIZE * memory[ptr].size;if (bufferSize > maxReadSize) {bufferSize = maxReadSize; // 限制读取大小}while (totalSize < bufferSize && currentBlock != -1) {int blockDataSize = min(BLOCK_SIZE, bufferSize - totalSize);memcpy(buffer + totalSize, memory[currentBlock].data, blockDataSize);totalSize += blockDataSize;currentBlock++; // 下一个块(假设连续分配)}return totalSize == bufferSize;}// 打印内存状态void printMemoryStatus() {cout << "内存状态:" << endl;for (int i = 0; i < memory.size(); ++i) {cout << "块 " << i << ": ";if (memory[i].status == USED) {cout << "已使用,大小: " << memory[i].size << " 块";} else {cout << "空闲,大小: " << memory[i].size << " 块,下一个: " << memory[i].next;}cout << endl;}cout << "空闲链表头: " << freeList << endl << endl;}
};// 测试内存管理器
int main() {// 创建一个有10个块,每个块32字节的内存管理器MemoryManager manager(10, 32);cout << "初始内存状态:" << endl;manager.printMemoryStatus();// 分配内存int ptr1 = manager.allocate(2); // 分配2个块cout << "分配2个块,指针: " << ptr1 << endl;manager.printMemoryStatus();int ptr2 = manager.allocate(3); // 分配3个块cout << "分配3个块,指针: " << ptr2 << endl;manager.printMemoryStatus();// 写入数据const char* message = "Hello, Memory Manager!";bool writeSuccess = manager.writeData(ptr1, message, strlen(message) + 1);cout << "写入数据" << (writeSuccess ? "成功" : "失败") << endl;// 读取数据char buffer[100];bool readSuccess = manager.readData(ptr1, buffer, 100);if (readSuccess) {cout << "读取的数据: " << buffer << endl;} else {cout << "读取数据失败" << endl;}// 释放内存manager.deallocate(ptr1);cout << "释放指针 " << ptr1 << " 后的内存状态:" << endl;manager.printMemoryStatus();// 再次分配内存int ptr3 = manager.allocate(4); // 分配4个块cout << "分配4个块,指针: " << ptr3 << endl;manager.printMemoryStatus();return 0;
}

10.3.2 指针模拟的应用场景

指针模拟在以下场景中非常有用:

  1. 内存受限的环境:如嵌入式系统,需要手动管理内存
  2. 自定义内存分配器:优化特定应用的内存使用
  3. 垃圾回收算法实现:跟踪对象引用关系
  4. 编程语言实现:在没有内置指针的语言中模拟指针功能
  5. 数据结构序列化:将包含指针的数据结构转换为可传输或存储的格式

10.4 有根树的表示

        树是一种重要的非线性数据结构,它由 n(n≥0)个节点组成。有根树是指其中一个节点被指定为根节点,其余节点都是根节点的后代。

10.4.1 树的表示方法

树有多种表示方法,各有优缺点,适用于不同的场景:

  1. 双亲表示法:每个节点存储其父节点的指针
  2. 孩子表示法:每个节点存储其所有子节点的指针列表
  3. 孩子 - 兄弟表示法:每个节点存储其第一个子节点和下一个兄弟节点的指针(最常用)
孩子 - 兄弟表示法(C++ 代码):

        孩子 - 兄弟表示法是最灵活的树表示方法,它可以很方便地将树转换为二叉树,从而利用二叉树的算法。

#include <iostream>
#include <vector>
#include <string>using namespace std;// 树节点结构(孩子-兄弟表示法)
template <typename T>
struct TreeNode {T data;               // 节点数据TreeNode* firstChild; // 指向第一个孩子节点TreeNode* nextSibling;// 指向相邻的下一个兄弟节点// 构造函数TreeNode(T val) : data(val), firstChild(nullptr), nextSibling(nullptr) {}
};// 树类
template <typename T>
class Tree {
private:TreeNode<T>* root; // 根节点// 递归销毁树void destroy(TreeNode<T>* node) {if (node == nullptr) return;// 先销毁所有孩子节点destroy(node->firstChild);// 再销毁所有兄弟节点destroy(node->nextSibling);// 最后销毁当前节点delete node;}// 递归前序遍历void preOrder(TreeNode<T>* node, vector<T>& result) const {if (node == nullptr) return;// 先访问当前节点result.push_back(node->data);// 再遍历所有孩子节点(通过firstChild和nextSibling)preOrder(node->firstChild, result);preOrder(node->nextSibling, result);}// 递归后序遍历void postOrder(TreeNode<T>* node, vector<T>& result) const {if (node == nullptr) return;// 先遍历所有孩子节点postOrder(node->firstChild, result);// 再访问当前节点result.push_back(node->data);// 最后遍历所有兄弟节点postOrder(node->nextSibling, result);}// 递归查找节点TreeNode<T>* findNode(TreeNode<T>* node, T val) const {if (node == nullptr) return nullptr;// 如果当前节点是要找的节点,返回它if (node->data == val) return node;// 先在孩子节点中查找TreeNode<T>* found = findNode(node->firstChild, val);if (found != nullptr) return found;// 再在兄弟节点中查找return findNode(node->nextSibling, val);}public:// 构造函数Tree() : root(nullptr) {}// 带根节点的构造函数Tree(T rootVal) : root(new TreeNode<T>(rootVal)) {}// 析构函数~Tree() {destroy(root);root = nullptr;}// 判断树是否为空bool isEmpty() const {return root == nullptr;}// 获取根节点TreeNode<T>* getRoot() const {return root;}// 设置根节点void setRoot(T val) {if (root != nullptr) {destroy(root);}root = new TreeNode<T>(val);}// 给指定节点添加子节点void addChild(TreeNode<T>* parent, T childVal) {if (parent == nullptr) return;TreeNode<T>* newNode = new TreeNode<T>(childVal);// 如果父节点没有孩子,直接作为第一个孩子if (parent->firstChild == nullptr) {parent->firstChild = newNode;} else {// 否则,找到最后一个孩子,添加为其兄弟TreeNode<T>* lastChild = parent->firstChild;while (lastChild->nextSibling != nullptr) {lastChild = lastChild->nextSibling;}lastChild->nextSibling = newNode;}}// 查找值为val的节点TreeNode<T>* find(T val) const {return findNode(root, val);}// 前序遍历(根 -> 孩子 -> 兄弟)vector<T> preOrderTraversal() const {vector<T> result;preOrder(root, result);return result;}// 后序遍历(孩子 -> 根 -> 兄弟)vector<T> postOrderTraversal() const {vector<T> result;postOrder(root, result);return result;}// 打印遍历结果void printTraversal(const vector<T>& traversal, const string& traversalName) const {cout << traversalName << "遍历: ";for (size_t i = 0; i < traversal.size(); ++i) {cout << traversal[i];if (i != traversal.size() - 1) {cout << " -> ";}}cout << endl;}
};// 测试树的功能
int main() {// 创建一棵树,根节点为"根"Tree<string> tree("根");// 获取根节点TreeNode<string>* root = tree.getRoot();// 给根节点添加子节点tree.addChild(root, "子节点1");tree.addChild(root, "子节点2");tree.addChild(root, "子节点3");// 找到"子节点1",给它添加子节点TreeNode<string>* child1 = tree.find("子节点1");if (child1 != nullptr) {tree.addChild(child1, "孙节点1-1");tree.addChild(child1, "孙节点1-2");}// 找到"子节点3",给它添加子节点TreeNode<string>* child3 = tree.find("子节点3");if (child3 != nullptr) {tree.addChild(child3, "孙节点3-1");}// 找到"孙节点3-1",给它添加子节点TreeNode<string>* grandchild31 = tree.find("孙节点3-1");if (grandchild31 != nullptr) {tree.addChild(grandchild31, "曾孙节点3-1-1");}// 遍历树vector<string> preOrder = tree.preOrderTraversal();tree.printTraversal(preOrder, "前序"); // 预期输出:前序遍历: 根 -> 子节点1 -> 孙节点1-1 -> 孙节点1-2 -> 子节点2 -> 子节点3 -> 孙节点3-1 -> 曾孙节点3-1-1vector<string> postOrder = tree.postOrderTraversal();tree.printTraversal(postOrder, "后序");// 预期输出:后序遍历: 孙节点1-1 -> 孙节点1-2 -> 子节点1 -> 子节点2 -> 曾孙节点3-1-1 -> 孙节点3-1 -> 子节点3 -> 根return 0;
}
树的孩子 - 兄弟表示法类图:
@startuml
title 树的孩子-兄弟表示法类图class TreeNode<T> {- T data- TreeNode<T>* firstChild- TreeNode<T>* nextSibling+ TreeNode(T val)
}class Tree<T> {- TreeNode<T>* root- void destroy(TreeNode<T>* node)- void preOrder(TreeNode<T>* node, vector<T>& result)- void postOrder(TreeNode<T>* node, vector<T>& result)- TreeNode<T>* findNode(TreeNode<T>* node, T val)+ Tree()+ Tree(T rootVal)+ ~Tree()+ bool isEmpty()+ TreeNode<T>* getRoot()+ void setRoot(T val)+ void addChild(TreeNode<T>* parent, T childVal)+ TreeNode<T>* find(T val)+ vector<T> preOrderTraversal()+ vector<T> postOrderTraversal()+ void printTraversal(const vector<T>&, const string&)
}Tree<T> "1" *-- "0..1" TreeNode<T> : has root
TreeNode<T> "1" *-- "0..1" TreeNode<T> : has firstChild
TreeNode<T> "1" *-- "0..1" TreeNode<T> : has nextSibling
@enduml

10.4.2 树的综合应用案例

案例:文件系统目录结构(树的应用)

文件系统是树结构的典型应用,我们可以用树来模拟文件系统的目录结构:

#include <iostream>
#include <vector>
#include <string>
#include <iomanip>using namespace std;// 文件/目录节点
struct FileNode {string name;          // 名称bool isDirectory;     // 是否为目录FileNode* firstChild; // 第一个子节点FileNode* nextSibling;// 下一个兄弟节点// 构造函数FileNode(string n, bool isDir) : name(n), isDirectory(isDir), firstChild(nullptr), nextSibling(nullptr) {}
};// 文件系统类
class FileSystem {
private:FileNode* root; // 根目录// 递归销毁节点void destroy(FileNode* node) {if (node == nullptr) return;// 先销毁所有子节点destroy(node->firstChild);// 再销毁所有兄弟节点destroy(node->nextSibling);// 最后销毁当前节点delete node;}// 递归添加节点bool addNode(FileNode* parent, FileNode* newNode) {if (parent == nullptr || !parent->isDirectory) {return false; // 父节点不存在或不是目录}// 如果父节点没有子节点,直接作为第一个子节点if (parent->firstChild == nullptr) {parent->firstChild = newNode;} else {// 否则,找到最后一个子节点,添加为其兄弟FileNode* lastChild = parent->firstChild;while (lastChild->nextSibling != nullptr) {lastChild = lastChild->nextSibling;}lastChild->nextSibling = newNode;}return true;}// 递归查找节点FileNode* findNode(FileNode* current, const string& name) {if (current == nullptr) return nullptr;// 如果当前节点是要找的节点,返回它if (current->name == name) return current;// 先在子节点中查找FileNode* found = findNode(current->firstChild, name);if (found != nullptr) return found;// 再在兄弟节点中查找return findNode(current->nextSibling, name);}// 递归打印目录结构void printStructure(FileNode* node, int depth) const {if (node == nullptr) return;// 打印缩进cout << setw(depth * 4) << "";// 打印节点信息if (node->isDirectory) {cout << "[" << node->name << "]" << endl;} else {cout << node->name << endl;}// 先打印子节点printStructure(node->firstChild, depth + 1);// 再打印兄弟节点printStructure(node->nextSibling, depth);}public:// 构造函数FileSystem() : root(new FileNode("/", true)) {}// 析构函数~FileSystem() {destroy(root);root = nullptr;}// 创建目录bool createDirectory(const string& parentDir, const string& dirName) {FileNode* parent = findNode(root, parentDir);return addNode(parent, new FileNode(dirName, true));}// 创建文件bool createFile(const string& parentDir, const string& fileName) {FileNode* parent = findNode(root, parentDir);return addNode(parent, new FileNode(fileName, false));}// 打印文件系统结构void printFileSystem() const {cout << "文件系统结构:" << endl;printStructure(root, 0);}
};int main() {FileSystem fs;// 创建目录结构fs.createDirectory("/", "home");fs.createDirectory("/", "usr");fs.createDirectory("/", "etc");fs.createDirectory("home", "user1");fs.createDirectory("home", "user2");fs.createDirectory("user1", "documents");fs.createDirectory("user1", "pictures");fs.createDirectory("usr", "bin");fs.createDirectory("usr", "lib");// 创建文件fs.createFile("documents", "report.txt");fs.createFile("documents", "notes.txt");fs.createFile("pictures", "photo1.jpg");fs.createFile("pictures", "photo2.png");fs.createFile("bin", "program1");fs.createFile("bin", "program2");fs.createFile("etc", "config.ini");// 打印文件系统结构fs.printFileSystem();return 0;
}

运行上述代码,将输出如下的文件系统结构:

http://www.dtcms.com/a/318886.html

相关文章:

  • 深入剖析Java线程:从基础到实战(上)
  • ubuntu cloud init 20.04LTS升级到22.04LTS
  • vue3接收SSE流数据进行实时渲染日志
  • Web开发模式 前端渲染 后端渲染 身份认证
  • 第三章:【springboot】框架介绍MyBatis
  • Spring AOP动态代理核心原理深度解析 - 图解+实战揭秘Java代理设计模式
  • 前端百分比展示导致后端 BigDecimal 转换异常的排查与解决
  • 多账号管理方案:解析一款免Root的App分身工具
  • 【RabbitMQ面试精讲 Day 13】HAProxy与负载均衡配置
  • HTTP 协议升级(HTTP Upgrade)机制
  • winform中的listbox实现拖拽功能
  • 基于ubuntu搭建gitlab
  • KDE Connect
  • 一篇文章入门TCP与UDP(保姆级别)
  • 02电气设计-安全继电器电路设计(让电路等级达到P4的安全等级)
  • C语言strncmp函数详解:安全比较字符串的实用工具
  • 合约收款方式,转账与问题安全
  • 怎么进行专项分析项目?
  • 上证50期权持仓明细在哪里查询?
  • C语言(08)——整数浮点数在内存中的存储
  • LINUX-批量文件管理及vim文件编辑器
  • 浅析 Berachain v2 ,对原有 PoL 机制进行了哪些升级?
  • AutoMQ-Kafka的替代方案实战
  • JAVA第六学:数组的使用
  • 【C++】哈希表原理与实现详解
  • 基于langchain的两个实际应用:[MCP多服务器聊天系统]和[解析PDF文档的RAG问答]
  • 智能制造的中枢神经工控机在自动化产线中的关键角色
  • 行业应用案例:MCP在不同垂直领域的落地实践
  • 二叉树算法之【中序遍历】
  • OpenAI重磅发布:GPT最新开源大模型gpt-oss系列全面解析