第3章数据结构

第3章:数据结构

3.1 基本数据结构

在这一小节中,我们讨论了几种最常见的基本数据结构,并介绍了它们的特点、操作方法和应用场景。

3.1.1 线性表 (Linear List)

定义:线性表是由一系列相同类型的元素按顺序排列而成的数据结构。最常见的线性表有数组和链表。 特点:元素之间存在一一对应的关系,可以通过下标访问每个元素。 线性表的两种常见实现

数组 (Array):固定大小的连续内存块,支持随机访问,但插入和删除操作效率较低。 链表 (Linked List):由一系列节点组成,每个节点包含数据和指向下一个节点的指针。链表适合插入和删除操作,但访问效率较低。

3.1.2 栈 (Stack)

定义:栈是一种后进先出(LIFO,Last In First Out)的数据结构。栈只有一个操作端,即栈顶,所有操作都在栈顶进行。

基本操作

push:将一个元素压入栈中。 pop:将栈顶元素移除。 toppeek:获取栈顶元素但不移除。 isEmpty:判断栈是否为空。

应用场景

函数调用:计算机通过栈来保存函数调用的上下文信息(如局部变量、返回地址等)。 表达式求值:栈常用于运算符优先级的管理,例如在计算中缀表达式时使用栈来转换为后缀表达式。

3.1.3 队列 (Queue)

定义:队列是一种先进先出(FIFO,First In First Out)的数据结构。队列有两个操作端,分别是队头和队尾。

基本操作

enqueue:将元素加入队尾。 dequeue:将队头元素移除。 front:获取队头元素但不移除。 isEmpty:判断队列是否为空。

应用场景

任务调度:操作系统使用队列来管理等待执行的任务。 广度优先搜索 (BFS):图算法中,队列用于管理待处理的节点。

3.2 链表 (Linked List)

3.2.1 单链表 (Singly Linked List)

定义:单链表是一种线性数据结构,其中每个节点包含一个数据域和一个指向下一个节点的指针。最后一个节点的指针指向 null,表示链表的结束。

操作

插入操作:在链表头部、尾部或指定位置插入节点。 删除操作:删除指定位置的节点。 查找操作:查找指定值的节点。

优点

  • 动态内存分配,不需要提前知道元素数量。
  • 插入和删除操作效率较高,尤其是在链表头部。

缺点

  • 不支持快速的随机访问。要访问某个节点,需要从头节点开始顺序遍历。
3.2.2 双向链表 (Doubly Linked List)

定义:双向链表是每个节点包含两个指针,一个指向前驱节点,一个指向后继节点。双向链表支持从两个方向进行遍历。 优点:可以在任意位置进行前向或后向遍历,插入和删除操作更加灵活。 缺点:比单链表需要更多的空间来存储前驱指针和后继指针。

3.3 栈的应用

栈有许多经典的应用,尤其在计算机科学中,栈在处理某些类型的递归、表达式计算和内存管理中非常重要。

3.3.1 括号匹配

问题:检查一个字符串中的括号是否正确匹配。 解决方法:通过栈来模拟括号的匹配。当遇到左括号时,将其压入栈中;当遇到右括号时,从栈中弹出对应的左括号。最终栈为空且没有多余的右括号,则括号匹配。

3.3.2 表达式求值

问题:计算算术表达式的值,尤其是后缀表达式。 解决方法:使用栈来保存操作数,当遇到运算符时,从栈中弹出操作数进行运算,再将结果压入栈中。

3.4 队列的应用

队列的应用主要体现在任务调度和图算法中。

3.4.1 任务调度

问题:操作系统中的进程调度常使用队列来管理等待执行的任务。任务按照先进先出的顺序被处理。

3.4.2 广度优先搜索 (BFS)

问题:在图算法中,广度优先搜索(BFS)使用队列来实现。BFS遍历图的每一层,使用队列来维护待访问的节点。

3.5 实现

在这一部分,书中会给出各种数据结构的实现代码。例如:

1. 栈的实现(使用链表)
   struct Node {
       int data;
       Node* next;
   };

   class Stack {
   private:
       Node* top;
   public:
       Stack() : top(nullptr) {}

       void push(int value) {
           Node* newNode = new Node{value, top};
           top = newNode;
       }

       int pop() {
           if (top == nullptr) throw std::out_of_range("Stack is empty");
           int value = top->data;
           Node* temp = top;
           top = top->next;
           delete temp;
           return value;
       }

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

2. 队列的实现(使用数组)

   class Queue {
   private:
       int* arr;
       int front, rear, capacity;
   public:
       Queue(int size) : capacity(size), front(0), rear(0) {
           arr = new int[size];
       }

       void enqueue(int value) {
           if ((rear + 1) % capacity == front) throw std::overflow_error("Queue is full");
           arr[rear] = value;
           rear = (rear + 1) % capacity;
       }

       int dequeue() {
           if (front == rear) throw std::underflow_error("Queue is empty");
           int value = arr[front];
           front = (front + 1) % capacity;
           return value;
       }

       bool isEmpty() {
           return front == rear;
       }
   };

线性表 (Linear List)

线性表是一种非常基础的、重要的数据结构,它由一组元素按照顺序排列而成。在 C++ 中,线性表常见的实现方式有 数组链表 两种形式。每种实现方式有不同的特点和适用场景。

1. 数组实现线性表

数组是线性表的一种实现,它提供了一个固定大小的连续内存空间来存储元素。数组支持随机访问,但插入和删除操作效率较低,尤其是当数组需要动态扩展时。

数组实现线性表

#include <iostream>
#include <stdexcept>

class ArrayList {
private:
    int* arr;           // 动态数组存储数据
    int capacity;       // 数组的容量
    int size;           // 当前存储的元素个数

public:
    // 构造函数
    ArrayList(int cap = 10) : capacity(cap), size(0) {
        arr = new int[capacity];  // 动态分配内存
    }

    // 析构函数
    ~ArrayList() {
        delete[] arr;  // 释放内存
    }

    // 添加元素到数组末尾
    void add(int value) {
        if (size >= capacity) {
            resize();  // 扩展数组容量
        }
        arr[size++] = value;
    }

    // 根据索引访问元素
    int get(int index) {
        if (index < 0 || index >= size) {
            throw std::out_of_range("Index out of bounds");
        }
        return arr[index];
    }

    // 删除指定索引位置的元素
    void remove(int index) {
        if (index < 0 || index >= size) {
            throw std::out_of_range("Index out of bounds");
        }
        for (int i = index; i < size - 1; ++i) {
            arr[i] = arr[i + 1];  // 向前移动元素
        }
        --size;
    }

    // 扩展数组容量
    void resize() {
        capacity *= 2;  // 容量翻倍
        int* newArr = new int[capacity];
        for (int i = 0; i < size; ++i) {
            newArr[i] = arr[i];
        }
        delete[] arr;  // 释放旧的数组
        arr = newArr;  // 更新为新的数组
    }

    // 打印所有元素
    void print() {
        for (int i = 0; i < size; ++i) {
            std::cout << arr[i] << " ";
        }
        std::cout << std::endl;
    }

    // 获取数组的当前大小
    int getSize() {
        return size;
    }

    // 获取数组的容量
    int getCapacity() {
        return capacity;
    }
};

int main() {
    ArrayList list;

    list.add(10);
    list.add(20);
    list.add(30);
    list.add(40);

    std::cout << "ArrayList after adding elements: ";
    list.print();

    std::cout << "Element at index 2: " << list.get(2) << std::endl;

    list.remove(2);

    std::cout << "ArrayList after removing element at index 2: ";
    list.print();

    std::cout << "Size: " << list.getSize() << ", Capacity: " << list.getCapacity() << std::endl;

    return 0;
}

解释:

  1. 构造函数和析构函数ArrayList 类通过动态分配内存来创建一个数组,并且在对象销毁时释放内存。
  2. 增加元素add 方法在数组末尾添加元素,当数组容量不足时,会调用 resize 方法扩展容量。
  3. 删除元素remove 方法删除指定索引位置的元素,删除时需要移动后面的元素来填补空位。
  4. 扩展数组容量:当元素个数超过数组容量时,resize 方法会将数组容量翻倍,并将原数组的元素复制到新的更大的数组中。
  5. 访问元素:通过 get 方法访问指定索引位置的元素,索引越界时会抛出异常。

适用场景:

  • 当数据量已知或变化较少时,可以使用数组实现线性表,快速随机访问元素。

2. 链表实现线性表

链表是通过指针连接的节点来实现的线性表。每个节点包含一个数据域和一个指向下一个节点的指针。链表的优点是可以动态地增加和删除元素,尤其是在头部或中间操作时,比数组更高效。

单链表实现线性表

#include <iostream>
#include <stdexcept>

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

    Node* head;  // 链表头
    int size;    // 当前存储的元素个数

public:
    // 构造函数
    LinkedList() : head(nullptr), size(0) {}

    // 析构函数
    ~LinkedList() {
        while (head != nullptr) {
            Node* temp = head;
            head = head->next;
            delete temp;
        }
    }

    // 添加元素到链表尾部
    void add(int value) {
        Node* newNode = new Node{value, nullptr};
        if (head == nullptr) {
            head = newNode;
        } else {
            Node* temp = head;
            while (temp->next != nullptr) {
                temp = temp->next;
            }
            temp->next = newNode;
        }
        ++size;
    }

    // 根据索引访问元素
    int get(int index) {
        if (index < 0 || index >= size) {
            throw std::out_of_range("Index out of bounds");
        }
        Node* temp = head;
        for (int i = 0; i < index; ++i) {
            temp = temp->next;
        }
        return temp->data;
    }

    // 删除指定索引位置的元素
    void remove(int index) {
        if (index < 0 || index >= size) {
            throw std::out_of_range("Index out of bounds");
        }
        Node* temp = head;
        if (index == 0) {
            head = head->next;
        } else {
            for (int i = 0; i < index - 1; ++i) {
                temp = temp->next;
            }
            Node* toDelete = temp->next;
            temp->next = temp->next->next;
            delete toDelete;
        }
        --size;
    }

    // 打印所有元素
    void print() {
        Node* temp = head;
        while (temp != nullptr) {
            std::cout << temp->data << " ";
            temp = temp->next;
        }
        std::cout << std::endl;
    }

    // 获取链表的当前大小
    int getSize() {
        return size;
    }
};

int main() {
    LinkedList list;

    list.add(10);
    list.add(20);
    list.add(30);
    list.add(40);

    std::cout << "LinkedList after adding elements: ";
    list.print();

    std::cout << "Element at index 2: " << list.get(2) << std::endl;

    list.remove(2);

    std::cout << "LinkedList after removing element at index 2: ";
    list.print();

    std::cout << "Size: " << list.getSize() << std::endl;

    return 0;
}

解释:

  1. 链表节点结构:每个节点包含一个数据域和一个指向下一个节点的指针(next)。
  2. 添加元素add 方法通过遍历链表,找到尾节点并将新节点链接到尾节点。
  3. 删除元素remove 方法删除指定位置的节点,考虑删除头节点和中间节点的情况。
  4. 访问元素:通过 get 方法遍历链表,返回指定索引位置的元素,若索引越界,抛出异常。

适用场景:

链表适合频繁进行插入和删除操作,特别是在中间或头部操作时比数组更高效。


总结

数组实现线性表:数组通过下标提供快速的随机访问,但插入和删除操作效率较低(尤其是需要扩展数组时)。 链表实现线性表:链表适合频繁的插入和删除操作,特别是在头部或中间部分操作时非常高效,但不支持快速的随机访问。

选择何种实现

数组:适合用于数据量已知且不需要频繁插入或删除元素的场景,如需要快速随机访问的应用。 链表:适合用于需要频繁插入和删除的场景,如任务调度、动态数据存储等。 在 C++ 标准库中,std::arraystd::list 都是常用的容器类型,它们分别是 静态数组双向链表 的实现。每个容器类型都具有自己独特的特性、优点和应用场景。在这部分,我们将详细介绍这两种容器的相关方法,并提供相应的示例。


1. std::array

std::array 是一个封装固定大小数组的容器,属于 C++11 引入的标准库。与传统的 C 风格数组不同,std::array 提供了更多的功能,例如容器接口和大小信息,能够更方便地操作固定大小的数组。

std::array 的特性

固定大小:一旦定义,std::array 的大小无法改变。 支持迭代器:可以像其他 STL 容器一样使用迭代器进行遍历。 集成 STL 算法:可以直接与 STL 算法(如 std::sort)配合使用。 与 C 风格数组兼容:底层是 C 风格数组,但提供了更多的成员函数来操作数据。

std::array 的常用方法

  1. begin()end():返回数组的开始和结束迭代器。
  2. size():返回数组的大小。
  3. at(index):返回指定索引位置的元素,并进行范围检查。
  4. front()back():分别返回数组的第一个元素和最后一个元素。
  5. fill(value):将所有元素填充为指定的值。
  6. swap(other):交换当前数组与另一个 std::array 的元素。
  7. data():返回指向数组元素的指针。

std::array 示例代码

#include <iostream>
#include <array>
#include <algorithm>

int main() {
    // 创建一个固定大小的 std::array
    std::array<int, 5> arr = {1, 2, 3, 4, 5};

    // 获取数组大小
    std::cout << "Size of array: " << arr.size() << std::endl;

    // 使用迭代器遍历数组
    std::cout << "Array elements: ";
    for (auto it = arr.begin(); it != arr.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    // 使用 at() 进行访问(带边界检查)
    std::cout << "Element at index 2: " << arr.at(2) << std::endl;

    // 获取第一个和最后一个元素
    std::cout << "First element: " << arr.front() << std::endl;
    std::cout << "Last element: " << arr.back() << std::endl;

    // 使用 fill() 填充所有元素
    arr.fill(10);
    std::cout << "Array after fill: ";
    for (int i : arr) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    // 使用 std::sort 对数组排序
    std::sort(arr.begin(), arr.end());
    std::cout << "Sorted array: ";
    for (int i : arr) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    // 交换两个 std::array
    std::array<int, 5> arr2 = {1, 1, 1, 1, 1};
    arr.swap(arr2);
    std::cout << "Array after swap: ";
    for (int i : arr) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    return 0;
}

输出

Size of array: 5
Array elements: 1 2 3 4 5 
Element at index 2: 3
First element: 1
Last element: 5
Array after fill: 10 10 10 10 10 
Sorted array: 10 10 10 10 10 
Array after swap: 1 1 1 1 1

总结:

  • std::array 是固定大小的数组封装,提供了更多的功能来替代传统的 C 风格数组。
  • 支持 STL 算法和迭代器,且能安全地进行索引操作(通过 at 方法)。

2. std::list

std::list 是 C++ STL 提供的双向链表容器。它是一个 动态大小 的容器,允许高效的在头部和中间进行元素的插入和删除操作,但缺点是访问元素的效率较低(需要线性时间遍历链表)。

std::list 的特性

双向链表:每个节点包含数据和指向前一个节点及下一个节点的指针。 动态大小:可以随时向 std::list 中添加或删除元素,大小可以动态变化。 支持迭代器:可以使用迭代器进行遍历操作。 高效的插入和删除操作:尤其是在头部、中间或尾部进行操作时非常高效。

std::list 的常用方法

  1. begin()end():返回指向第一个元素和最后一个元素之后的迭代器。
  2. size():返回当前列表的元素个数。
  3. push_front()push_back():分别在头部和尾部添加元素。
  4. pop_front()pop_back():分别从头部和尾部删除元素。
  5. front()back():分别返回头部和尾部的元素。
  6. insert():在指定位置插入元素。
  7. erase():删除指定位置的元素。
  8. clear():删除所有元素。
  9. sort():对列表元素进行排序。
  10. remove():删除指定值的所有元素。

std::list 示例代码

#include <iostream>
#include <list>
#include <algorithm>

int main() {
    // 创建一个 std::list
    std::list<int> lst = {10, 20, 30, 40, 50};

    // 获取列表大小
    std::cout << "Size of list: " << lst.size() << std::endl;

    // 在列表尾部添加元素
    lst.push_back(60);
    // 在列表头部添加元素
    lst.push_front(5);

    std::cout << "List after pushing elements: ";
    for (int val : lst) {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    // 删除头部元素
    lst.pop_front();
    // 删除尾部元素
    lst.pop_back();

    std::cout << "List after popping elements: ";
    for (int val : lst) {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    // 使用 insert 插入元素到指定位置
    auto it = lst.begin();
    std::advance(it, 2);  // 移动迭代器到索引 2
    lst.insert(it, 25);

    std::cout << "List after insertion: ";
    for (int val : lst) {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    // 使用 remove 删除特定元素
    lst.remove(30);  // 删除所有值为 30 的元素

    std::cout << "List after removing 30: ";
    for (int val : lst) {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    // 对列表进行排序
    lst.sort();
    std::cout << "Sorted list: ";
    for (int val : lst) {
        std::cout << val << " ";
    }
    std::cout << std::endl;

    // 清空列表
    lst.clear();
    std::cout << "List after clear: " << lst.size() << std::endl;

    return 0;
}

输出

Size of list: 5
List after pushing elements: 5 10 20 30 40 50 60 
List after popping elements: 10 20 30 40 50 
List after insertion: 10 20 25 30 40 50 
List after removing 30: 10 20 25 40 50 
Sorted list: 10 20 25 40 50 
List after clear: 0

总结:

std::list 是一个双向链表,适合频繁进行插入和删除操作。 * 不支持随机访问,所有访问操作的时间复杂度是 O(n)