C++ 数据结构 单链表、双链表、循环单向链表、循环双向链表

单链表和双链表是常用的线性数据结构,它们都有各自的优缺点和适用场景。以下是它们的基本概念、实现示例以及各自的特点。

单链表

概念

单链表是由一系列节点组成的线性数据结构,每个节点包含数据和一个指向下一个节点的指针。最后一个节点的指针指向nullptr

特点
  • 插入和删除效率:在已知位置的情况下,插入和删除操作的时间复杂度为O(1),但查找时间复杂度为O(n)。
  • 内存占用:每个节点只存储一个指针,相对较省内存。
  • 单向性:只能向一个方向遍历。
操作示意

插入操作

  • 在链表头插入 (插入1):

    cpp 复制代码
    [1] -> nullptr

    在尾部插入 (插入2):

cpp 复制代码
[1] -> [2] -> nullptr

在中间插入 (插入3,索引1):

cpp 复制代码
[1] -> [3] -> [2] -> nullptr

删除操作

  • 删除索引1的元素 (删除3):

    cpp 复制代码
    [1] -> [2] -> nullptr
    cpp 复制代码
    #include <iostream>
    
    // 单链表节点类
    template <typename T>
    class Node {
    public:
        T data;        // 节点的数据部分
        Node* next;    // 指向下一个节点的指针
    
        Node(const T& value) : data(value), next(nullptr) {} // 构造函数
    };
    
    // 单链表类
    template <typename T>
    class SinglyLinkedList {
    public:
        SinglyLinkedList() : head(nullptr) {} // 构造函数,初始化头指针为空
    
        ~SinglyLinkedList() {
            while (head) {
                remove(0); // 删除所有节点
            }
        }
    
        // 在指定索引处插入元素
        void insert(int index, const T& value) {
            if (index < 0) return; // 索引无效
    
            Node<T>* newNode = new Node<T>(value); // 创建新节点
            if (index == 0) {
                // 插入到头部
                newNode->next = head; // 新节点指向当前头节点
                head = newNode; // 更新头指针为新节点
                return;
            }
    
            Node<T>* current = head;
            // 找到插入位置的前一个节点
            for (int i = 0; i < index - 1 && current != nullptr; ++i) {
                current = current->next; // 移动到下一个节点
            }
    
            if (current) {
                // 在中间插入
                newNode->next = current->next; // 新节点指向插入位置的下一个节点
                current->next = newNode; // 前一个节点指向新节点
            } else {
                delete newNode; // 超出范围,释放内存
            }
        }
    
        // 删除指定索引的元素
        bool remove(int index) {
            if (index < 0 || head == nullptr) return false; // 索引无效或链表为空
    
            Node<T>* temp = head; // 从头节点开始
    
            if (index == 0) {
                // 删除头节点
                head = temp->next; // 更新头指针为下一个节点
                delete temp; // 释放内存
                return true;
            }
    
            // 找到要删除节点的前一个节点
            for (int i = 0; i < index - 1 && temp != nullptr; ++i) {
                temp = temp->next; // 移动到下一个节点
            }
    
            if (temp == nullptr || temp->next == nullptr) return false; // 超出范围
    
            // 删除目标节点
            Node<T>* nodeToDelete = temp->next; // 保存要删除的节点
            temp->next = nodeToDelete->next; // 前一个节点指向目标节点的下一个节点
            delete nodeToDelete; // 释放内存
    
            return true; // 删除成功
        }
    
        // 打印链表中的所有元素
        void print() const {
            Node<T>* current = head; // 从头节点开始
            while (current) {
                std::cout << current->data << " "; // 输出节点数据
                current = current->next; // 移动到下一个节点
            }
            std::cout << std::endl; // 换行
        }
    
    private:
        Node<T>* head; // 链表的头指针
    };
    
    int main() {
        SinglyLinkedList<int> list; // 创建单链表实例
        list.insert(0, 1); // 在索引0插入1
        list.insert(1, 2); // 在索引1插入2
        list.insert(1, 3); // 在索引1插入3
        list.print(); // 输出: 1 3 2
    
        list.remove(1); // 删除索引1的元素
        list.print(); // 输出: 1 2
    
        return 0;
    }

    双链表

    概念

    双链表是由一系列节点组成的线性数据结构,每个节点包含数据、一个指向下一个节点的指针和一个指向前一个节点的指针。第一个节点的前指针和最后一个节点的后指针均指向nullptr

    特点
  • 双向遍历:可以从前向后和从后向前遍历。

  • 插入和删除效率:与单链表相同,但双链表在删除时可以更方便地找到前驱节点。

  • 内存占用:每个节点需存储两个指针,因此占用更多内存。

操作示意

插入操作

  • 在链表头插入 (插入1)
cpp 复制代码
[1] <-> nullptr

在尾部插入 (插入2)

cpp 复制代码
[1] <-> [2] <-> nullptr

在中间插入 (插入3,索引1)

cpp 复制代码
[1] <-> [3] <-> [2] <-> nullptr

删除操作

  • 删除索引1的元素 (删除3)
cpp 复制代码
[1] <-> [2] <-> nullptr
cpp 复制代码
#include <iostream>

// 双链表节点类
template <typename T>
class DNode {
public:
    T data;        // 节点的数据部分
    DNode* next;   // 指向下一个节点的指针
    DNode* prev;   // 指向前一个节点的指针

    DNode(const T& value) : data(value), next(nullptr), prev(nullptr) {} // 构造函数
};

// 双链表类
template <typename T>
class DoublyLinkedList {
public:
    DoublyLinkedList() : head(nullptr), tail(nullptr) {} // 构造函数,初始化头和尾指针为空

    ~DoublyLinkedList() {
        while (head) {
            remove(0); // 删除所有节点
        }
    }

    // 在指定索引处插入元素
    void insert(int index, const T& value) {
        if (index < 0) return; // 索引无效

        DNode<T>* newNode = new DNode<T>(value); // 创建新节点
        if (index == 0) {
            // 插入到头部
            newNode->next = head; // 新节点指向当前头节点
            if (head) head->prev = newNode; // 旧头节点的前驱指向新节点
            head = newNode; // 更新头指针为新节点
            if (!tail) tail = newNode; // 如果是第一个节点,更新尾指针
            return;
        }

        DNode<T>* current = head;
        // 找到插入位置的前一个节点
        for (int i = 0; i < index - 1 && current != nullptr; ++i) {
            current = current->next; // 移动到下一个节点
        }

        if (current) {
            // 在中间插入
            newNode->next = current->next; // 新节点指向插入位置的下一个节点
            newNode->prev = current; // 新节点的前驱指向当前节点
            if (current->next) current->next->prev = newNode; // 旧下一个节点的前驱指向新节点
            current->next = newNode; // 当前节点指向新节点
            if (!newNode->next) tail = newNode; // 如果是最后一个节点,更新尾指针
        } else {
            delete newNode; // 超出范围,释放内存
        }
    }

    // 删除指定索引的元素
    bool remove(int index) {
        if (index < 0 || head == nullptr) return false; // 索引无效或链表为空

        DNode<T>* temp = head; // 从头节点开始

        if (index == 0) {
            // 删除头节点
            head = temp->next; // 更新头指针为下一个节点
            if (head) head->prev = nullptr; // 更新新头节点的前驱指针
            else tail = nullptr; // 如果链表空了,更新尾指针
            delete temp; // 释放内存
            return true;
        }

        // 找到要删除节点的前一个节点
        for (int i = 0; i < index && temp != nullptr; ++i) {
            temp = temp->next; // 移动到下一个节点
        }

        if (temp == nullptr) return false; // 超出范围

        // 删除目标节点
        if (temp->prev) temp->prev->next = temp->next; // 前一个节点指向目标节点的下一个节点
        if (temp->next) temp->next->prev = temp->prev; // 下一个节点的前驱指向前一个节点
        if (temp == tail) tail = temp->prev; // 如果删除的是尾节点,更新尾指针

        delete temp; // 释放内存
        return true; // 删除成功
    }

    // 打印链表中的所有元素
    void print() const {
        DNode<T>* current = head; // 从头节点开始
        while (current) {
            std::cout << current->data << " "; // 输出节点数据
            current = current->next; // 移动到下一个节点
        }
        std::cout << std::endl; // 换行
    }

private:
    DNode<T>* head; // 链表的头指针
    DNode<T>* tail; // 链表的尾指针
};

int main() {
    DoublyLinkedList<int> list; // 创建双链表实例
    list.insert(0, 1); // 在索引0插入1
    list.insert(1, 2); // 在索引1插入2
    list.insert(1, 3); // 在索引1插入3
    list.print(); // 输出: 1 3 2

    list.remove(1); // 删除索引1的元素
    list.print(); // 输出: 1 2

    return 0;
}

单循环链表 (Circular Singly Linked List)

特性

  1. 循环性 :最后一个节点的 next 指针指向头节点,形成一个环形结构。
  2. 单向遍历:只能从头节点开始向前遍历,无法反向访问。
  3. 动态大小:可以动态增加或减少节点,内存利用灵活。
  4. 不易删除:无法直接访问前一个节点,因此删除操作需要遍历链表。

操作顺序

  1. 插入
    • 创建新节点。
    • 如果链表为空,设置头节点为新节点,next 指向自身。
    • 如果不为空,找到尾节点,将新节点插入到头前。
    • 更新尾节点的 next 指针指向新节点。
  2. 删除
    • 遍历链表找到目标节点及其前驱节点。
    • 调整前驱节点的 next 指向目标节点的 next,以跳过目标节点。
    • 释放目标节点的内存。
操作示意
  1. 插入操作

    • 在链表头插入 (插入3)

      cpp 复制代码
      [3] <-> [3]

      在链表头插入 (插入2)

      cpp 复制代码
      [2] <-> [3] <-> [2]

      在链表头插入 (插入1)

      cpp 复制代码
      [1] <-> [2] <-> [3] <-> [1]

删除操作

操作顺序

  • 删除值为2的元素

    cpp 复制代码
    [1] <-> [3] <-> [1]
    cpp 复制代码
    #include <iostream>
    
    // 循环双链表节点类
    template <typename T>
    class DNode {
    public:
        T data;        // 节点的数据部分
        DNode* next;   // 指向下一个节点的指针
        DNode* prev;   // 指向前一个节点的指针
    
        DNode(const T& value) : data(value), next(nullptr), prev(nullptr) {} // 构造函数
    };
    
    // 循环双链表类
    template <typename T>
    class CircularDoublyLinkedList {
    public:
        CircularDoublyLinkedList() : head(nullptr) {} // 构造函数,初始化头指针为空
    
        ~CircularDoublyLinkedList() {
            if (head) {
                DNode<T>* current = head;
                DNode<T>* nextNode;
                do {
                    nextNode = current->next; // 保存下一个节点
                    delete current; // 删除当前节点
                    current = nextNode; // 移动到下一个节点
                } while (current != head); // 直到回到头节点
            }
        }
    
        // 在链表头插入元素
        void insertAtHead(const T& value) {
            DNode<T>* newNode = new DNode<T>(value); // 创建新节点
            if (!head) {
                head = newNode; // 如果链表为空,头指针指向新节点
                head->next = head; // 新节点指向自己
                head->prev = head; // 新节点的前驱指向自己
            } else {
                newNode->next = head; // 新节点指向当前头节点
                newNode->prev = head->prev; // 新节点的前驱指向尾节点
                head->prev->next = newNode; // 尾节点指向新节点
                head->prev = newNode; // 新节点成为新的头节点
                head = newNode; // 更新头指针为新节点
            }
        }
    
        // 删除指定值的元素
        bool remove(const T& value) {
            if (!head) return false; // 链表为空
    
            DNode<T>* current = head;
            do {
                if (current->data == value) {
                    if (current == head) {
                        // 删除头节点
                        if (current->next == head) {
                            delete current; // 只有一个节点
                            head = nullptr; // 更新头指针为空
                        } else {
                            head->prev->next = head->next; // 尾节点指向新头节点
                            head->next->prev = head->prev; // 新头节点的前驱指向尾节点
                            DNode<T>* temp = head; // 保存当前头节点
                            head = head->next; // 更新头指针
                            delete temp; // 删除旧头节点
                        }
                    } else {
                        // 删除中间或尾节点
                        current->prev->next = current->next; // 前一个节点跳过当前节点
                        current->next->prev = current->prev; // 后一个节点跳过当前节点
                        delete current; // 删除当前节点
                    }
                    return true; // 删除成功
                }
                current = current->next; // 移动到下一个节点
            } while (current != head); // 直到回到头节点
    
            return false; // 未找到元素
        }
    
        // 打印链表中的所有元素
        void print() const {
            if (!head) return; // 链表为空
    
            DNode<T>* current = head; // 从头节点开始
            do {
                std::cout << current->data << " "; // 输出节点数据
                current = current->next; // 移动到下一个节点
            } while (current != head); // 直到回到头节点
            std::cout << std::endl; // 换行
        }
    
    private:
        DNode<T>* head; // 链表的头指针
    };
    
    int main() {
        CircularDoublyLinkedList<int> list; // 创建循环双链表实例
        list.insertAtHead(1); // 在头插入1
        list.insertAtHead(2); // 在头插入2
        list.insertAtHead(3); // 在头插入3
        list.print(); // 输出: 3 2 1
    
        list.remove(2); // 删除值为2的节点
        list.print(); // 输出: 3 1
    
        return 0;
    }

    双循环链表 (Circular Doubly Linked List)

    特性

  • 循环性 :尾节点的 next 指针指向头节点,头节点的 prev 指针指向尾节点,形成环形结构。

  • 双向遍历:可以从任意节点向前或向后遍历。

  • 插入

    • 创建新节点。
    • 如果链表为空,设置头节点为新节点,nextprev 均指向自身。
    • 如果不为空:
      • 更新新节点的 nextprev 指向相应的节点。
      • 更新相关节点的指针以保持循环结构。
  • 删除

    • 遍历链表找到目标节点。
    • 调整前驱节点的 next 和后继节点的 prev 指针,以跳过目标节点。
    • 释放目标节点的内存。
  • 动态大小:和单循环链表一样,动态增加或减少节点。

  • 插入和删除灵活:可以在任何位置方便地进行插入和删除操作,因为每个节点都有前驱和后继指针。

cpp 复制代码
#include <iostream>

// 循环双链表节点类
template <typename T>
class DNode {
public:
    T data;        // 节点的数据部分
    DNode* next;   // 指向下一个节点的指针
    DNode* prev;   // 指向前一个节点的指针

    DNode(const T& value) : data(value), next(nullptr), prev(nullptr) {} // 构造函数
};

// 循环双链表类
template <typename T>
class CircularDoublyLinkedList {
public:
    CircularDoublyLinkedList() : head(nullptr) {} // 构造函数,初始化头指针为空

    ~CircularDoublyLinkedList() {
        if (head) {
            DNode<T>* current = head;
            DNode<T>* nextNode;
            do {
                nextNode = current->next; // 保存下一个节点
                delete current; // 删除当前节点
                current = nextNode; // 移动到下一个节点
            } while (current != head); // 直到回到头节点
        }
    }

    // 在链表头插入元素
    void insertAtHead(const T& value) {
        DNode<T>* newNode = new DNode<T>(value); // 创建新节点
        if (!head) {
            head = newNode; // 如果链表为空,头指针指向新节点
            head->next = head; // 新节点指向自己
            head->prev = head; // 新节点的前驱指向自己
        } else {
            newNode->next = head; // 新节点指向当前头节点
            newNode->prev = head->prev; // 新节点的前驱指向尾节点
            head->prev->next = newNode; // 尾节点指向新节点
            head->prev = newNode; // 新节点成为新的头节点
            head = newNode; // 更新头指针为新节点
        }
    }

    // 删除指定值的元素
    bool remove(const T& value) {
        if (!head) return false; // 链表为空

        DNode<T>* current = head;
        do {
            if (current->data == value) {
                if (current == head) {
                    // 删除头节点
                    if (current->next == head) {
                        delete current; // 只有一个节点
                        head = nullptr; // 更新头指针为空
                    } else {
                        head->prev->next = head->next; // 尾节点指向新头节点
                        head->next->prev = head->prev; // 新头节点的前驱指向尾节点
                        DNode<T>* temp = head; // 保存当前头节点
                        head = head->next; // 更新头指针
                        delete temp; // 删除旧头节点
                    }
                } else {
                    // 删除中间或尾节点
                    current->prev->next = current->next; // 前一个节点跳过当前节点
                    current->next->prev = current->prev; // 后一个节点跳过当前节点
                    delete current; // 删除当前节点
                }
                return true; // 删除成功
            }
            current = current->next; // 移动到下一个节点
        } while (current != head); // 直到回到头节点

        return false; // 未找到元素
    }

    // 打印链表中的所有元素
    void print() const {
        if (!head) return; // 链表为空

        DNode<T>* current = head; // 从头节点开始
        do {
            std::cout << current->data << " "; // 输出节点数据
            current = current->next; // 移动到下一个节点
        } while (current != head); // 直到回到头节点
        std::cout << std::endl; // 换行
    }

private:
    DNode<T>* head; // 链表的头指针
};

int main() {
    CircularDoublyLinkedList<int> list; // 创建循环双链表实例
    list.insertAtHead(1); // 在头插入1
    list.insertAtHead(2); // 在头插入2
    list.insertAtHead(3); // 在头插入3
    list.print(); // 输出: 3 2 1

    list.remove(2); // 删除值为2的节点
    list.print(); // 输出: 3 1

    return 0;
}

总结

下面是关于单链表、双链表、单向循环链表和双向循环链表的操作顺序的详细总结。

1. 单链表 (Singly Linked List)

在头插入

  1. 创建新节点,设置新节点的数据。
  2. 将新节点的 next 指向当前头节点。
  3. 更新头指针为新节点。

在尾插入

  1. 创建新节点,设置新节点的数据。
  2. 如果链表为空,设置头指针为新节点,next 指向 nullptr
  3. 如果链表不为空,遍历链表找到最后一个节点。
  4. 将最后一个节点的 next 指向新节点,并将新节点的 next 设置为 nullptr

在中间插入

  1. 创建新节点,设置新节点的数据。
  2. 遍历链表,找到插入位置的前一个节点。
  3. 将新节点的 next 指向前一个节点的 next
  4. 更新前一个节点的 next 指向新节点。

删除操作


2. 双链表 (Doubly Linked List)

删除操作


3. 单向循环链表 (Circular Singly Linked List)

删除操作


4. 双向循环链表 (Circular Doubly Linked List)

删除操作

  • 删除头节点

    1. 如果链表为空,返回失败。
    2. 保存当前头节点,更新头指针为头节点的 next
    3. 删除保存的头节点。
  • 删除尾节点

    1. 如果链表为空,返回失败。
    2. 如果链表只有一个节点,调用删除头节点。
    3. 遍历链表找到倒数第二个节点。
    4. 更新倒数第二个节点的 nextnullptr,删除尾节点。
  • 删除中间节点

    1. 遍历链表找到目标节点及其前驱节点。
    2. 更新前驱节点的 next 指向目标节点的 next
    3. 删除目标节点。
  • 在头插入

    1. 创建新节点,设置新节点的数据。
    2. 如果链表为空,设置头指针为新节点,nextprev 均指向自身。
    3. 如果链表不为空,设置新节点的 next 指向当前头节点。
    4. 更新当前头节点的 prev 指向新节点。
    5. 新节点的 prev 指向尾节点,更新尾节点的 next 指向新节点。
    6. 更新头指针为新节点。
  • 在尾插入

    1. 创建新节点,设置新节点的数据。
    2. 如果链表为空,调用在头插入。
    3. 如果链表不为空,遍历找到尾节点。
    4. 设置新节点的 prev 指向当前尾节点,更新尾节点的 next 指向新节点。
    5. 更新新节点的 next 指向头节点,头节点的 prev 指向新节点。
  • 在中间插入

    1. 创建新节点,设置新节点的数据。
    2. 遍历找到插入位置的前一个节点。
    3. 更新新节点的 next 指向前一个节点的 next,并更新前一个节点的 next 指向新节点。
    4. 更新新节点的 prev 指向前一个节点。
    5. 更新新节点的 nextprev 指向新节点。
  • 删除头节点

    1. 如果链表为空,返回失败。
    2. 保存当前头节点。
    3. 更新头指针为头节点的 next
    4. 更新新头节点的 prev 指向尾节点。
    5. 删除保存的头节点。
  • 删除尾节点

    1. 如果链表为空,返回失败。
    2. 如果链表只有一个节点,调用删除头节点。
    3. 找到倒数第二个节点,更新其 nextnullptr
    4. 更新新尾节点的 next 指向头节点,头节点的 prev 指向新尾节点。
    5. 删除原尾节点。
  • 删除中间节点

    1. 遍历链表找到目标节点。
    2. 更新目标节点的 prevnextnextprev 指向相应的节点,跳过目标节点。
    3. 删除目标节点。
  • 在头插入

    1. 创建新节点,设置数据。
    2. 如果链表为空,设置新节点的 next 指向自身,头指针指向新节点。
    3. 如果不为空,找到尾节点,更新尾节点的 next 为新节点,新节点的 next 为头节点。
    4. 更新头指针为新节点。
  • 在尾插入

    1. 创建新节点,设置数据。
    2. 如果链表为空,调用在头插入。
    3. 如果不为空,找到尾节点,更新尾节点的 next 为新节点,新节点的 next 为头节点。
  • 在中间插入

    1. 创建新节点,设置数据。
    2. 遍历找到插入位置的前一个节点。
    3. 更新新节点的 next 指向前一个节点的 next,并更新前一个节点的 next 指向新节点。
  • 删除头节点

    1. 如果链表为空,返回失败。
    2. 保存当前头节点。
    3. 更新头指针为头节点的 next
    4. 找到尾节点,更新尾节点的 next 为新头节点。
    5. 删除保存的头节点。
  • 删除尾节点

    1. 如果链表为空,返回失败。
    2. 如果链表只有一个节点,调用删除头节点。
    3. 遍历找到倒数第二个节点,更新其 next 指向头节点,删除尾节点。
  • 删除中间节点

    1. 遍历找到目标节点及其前驱节点。
    2. 更新前驱节点的 next 指向目标节点的 next
    3. 删除目标节点。
  • 在头插入

    1. 创建新节点,设置数据。
    2. 如果链表为空,设置新节点的 nextprev 均指向自身,头指针指向新节点。
    3. 如果不为空,更新新节点的 next 为头节点,prev 为尾节点。
    4. 更新头节点的 prev 为新节点,尾节点的 next 为新节点。
    5. 更新头指针为新节点。
  • 在尾插入

    1. 创建新节点,设置数据。
    2. 如果链表为空,调用在头插入。
    3. 如果不为空,更新新节点的 prev 为当前尾节点,next 为头节点。
    4. 更新当前尾节点的 next 为新节点,头节点的 prev 为新节点。
  • 在中间插入

    1. 创建新节点,设置数据。
    2. 遍历找到插入位置的前一个节点。
    3. 更新新节点的 nextprev 指针,并更新相关节点的指针以保持循环结构。
  • 删除头节点

    1. 如果链表为空,返回失败。
    2. 保存当前头节点。
    3. 更新头指针为头节点的 next
    4. 更新新头节点的 prev 指向尾节点,尾节点的 next 指向新头节点。
    5. 删除保存的头节点。
  • 删除尾节点

    1. 如果链表为空,返回失败。
    2. 如果链表只有一个节点,调用删除头节点。
    3. 更新倒数第二个节点的 next 为头节点,头节点的 prev 指向新尾节点。
    4. 删除原尾节点。
  • 删除中间节点

    1. 遍历链表找到目标节点。
    2. 更新目标节点的 prevnext 的指针以跳过目标节点。
    3. 删除目标节点。
相关推荐
爱吃生蚝的于勒14 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~18 分钟前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
脉牛杂德1 小时前
多项式加法——C语言
数据结构·c++·算法
legend_jz1 小时前
STL--哈希
c++·算法·哈希算法
CSUC1 小时前
【C++】父类参数有默认值时子类构造函数列表中可以省略该参数
c++
Vanranrr1 小时前
C++ QT
java·c++·qt
鸿儒5171 小时前
C++ lambda 匿名函数
开发语言·c++
一直学习永不止步1 小时前
LeetCode题练习与总结:赎金信--383
java·数据结构·算法·leetcode·字符串·哈希表·计数
van叶~2 小时前
算法妙妙屋-------1.递归的深邃回响:二叉树的奇妙剪枝
c++·算法
knighthood20013 小时前
解决:ros进行gazebo仿真,rviz没有显示传感器数据
c++·ubuntu·ros