【C++数据结构】关于单链表,双向链表,循环链表,静态链表,跳表的一些实现

前言

  • 接下来一段时间博主将会更新一个系列使用C++实现编程的五大池,而首当其冲的就是内存池,在实现内存池的过程中我们会使用到单向链表双向链表,为此我觉得一些关于基础数据结构的内容可以单独分出来讲一期,所以就有了今天这一篇文章。(绝对不是为了水文章)
  • 那么我们这一期就来看看链表的一些实现吧。

1 链表

1-1 什么是链表
  • 链表是一种动态数据结构,由一系列节点(Node)组成,每个节点存储数据并指向下一个节点。链表不像数组那样使用连续的内存空间,它通过指针链接各个节点,支持高效的插入和删除操作。
1-2 链表的基本特点

动态内存分配:按需分配内存,不需要事先指定大小。
高效插入/删除:在任意位置插入/删除节点的时间复杂度为 O(1)(给定指针的情况下)。
不连续存储:不像数组,链表的元素存储在非连续的内存地址,依靠指针连接。
访问速度较慢:无法像数组那样通过索引 O(1) 访问,需要 O(n) 遍历。
额外空间开销:每个节点需要额外存储指针,增加了内存使用。

1-3 链表的分类
链表类型结构特点优缺点适用场景
单链表(Singly Linked List)每个节点指向下一个节点✅ 实现简单,占用空间少
❌ 只能单向遍历
适用于一般场景,如简单数据存储
双向链表(Doubly Linked List)每个节点有前后两个指针✅ 支持双向遍历,插入/删除更高效
❌ 占用额外空间
适用于需要频繁插入/删除的数据结构
循环链表(Circular Linked List)尾节点指向头节点形成环✅ 适用于循环任务,如 CPU 调度
❌ 需要额外逻辑避免死循环
适用于循环缓冲区、任务调度
跳表(Skip List)在链表基础上增加多级索引✅ 查找速度接近 O(log n)
❌ 结构复杂,维护成本高
适用于有序数据的快速查找,如数据库索引
1-4 链表VS数组
特性链表数组
内存分布非连续,动态分配连续存储
插入/删除O(1)(给定指针)O(n)(需要移动元素)
查找速度O(n)(需要遍历)O(1)(随机访问)
额外开销额外指针存储仅存储数据,无额外开销
扩展性动态扩展,不受大小限制需要重新分配内存
  • 如果你的应用频繁插入/删除,推荐使用链表,而如果访问速度优先,建议使用数组。(也就是为什么我们后续使用内存池会使用链表辣!!)

2 单向链表

2-1 概念
  • 单向链表(Singly Linked List)是一种线性数据结构,由 一系列节点(Node) 组成,每个节点包含两个部分:

    • 数据域(Data):存储实际数据。
    • 指针域(Next):存储指向下一个节点的指针。
      请添加图片描述
  • 在C++中,我们可以使用一个模板结构体来存储一个节点的数据及其它指向下一个节点的指针:

template <typename T>
class Node {
public:
    T data;           // 存储数据
    Node* next;       // 指向下一个节点

    Node(T val) : data(val), next(nullptr) {} 
};
2-2 链表-头插
  • 下述是一段链表头插的代码,不急,我们一步步分析
 // 头部插入
void insertFront(T value) {
	Node<T>* newNode = new Node<T>(value);
	newNode->next = head;
	head = newNode;
}
  • 与其直接上复杂的说明,不然来个浅显易懂的例子:
2-2-1 例子引入
  • 假设我们有一个链表,我们需要插入一个元素5
head → [10] → [20] → [30] → nullptr
2-2-1 创建新的节点
  • 我们搜先需要创建一个新的节点:
Node<T>* newNode = new Node<T>(value);
  • newNode 现在是一个独立的节点:
[5] → nullptr
2-2-2 更新指针
  • 接下来我们需要更新新元素的指针,让新元素指向下一个元素的位置(也就是需要插入的位置)
newNode->next = head;
  • 让新节点的 next 指向 head,即 [10] 的位置:
[5] → [10] → [20] → [30] → nullptr
2-2-3 更新头指针header
  • 聪明的你肯定明白了,下一步需要让header重新指向新的元素5
head = newNode;
  • 也就有了:
head → [5] → [10] → [20] → [30] → nullptr
2-2-4 完整流程图
插入新节点 5
next
head = newNode
10
5
head
20
30
nullptr
2-2-5 同样道理理解第一个元素被插入时候的亚子
插入新节点 10
nullptr
next
head = newNode
nullptr
10
head
空链表

2-3 链表尾部插入
  • 我们再来看看尾部插入,尾部插入的过程可以分为两种情况:
  1. 链表为空时,插入第一个元素
  2. 链表非空时,找到尾部并插入(这里利用了单向链表尾部元素节点内的指针为空来找寻)
void insertBack(T value) {
    Node<T>* newNode = new Node<T>(value);
    if (!head) {   // 情况 1:链表为空
        head = newNode;
        return;
    }
    Node<T>* temp = head;   // 让 temp 指向 head
    while (temp->next) {    // 遍历找到最后一个节点
        temp = temp->next;
    }
    temp->next = newNode;   // 让最后一个节点的 next 指向新节点
}

2-4 链表元素删除
  • 我们再来看看元素的删除
// 删除节点
void remove(T value) {
	
	if (!head) return;

	
	if (head->data == value) {
		Node<T>* temp = head;
		head = head->next;
		delete temp;
		return;
	}

	//用于遍历链表找到目标节点
	Node<T>* current = head;
	while (current->next && current->next->data != value) {
		current = current->next;
	}

	if (current->next) {
		Node<T>* temp = current->next;
		current->next = current->next->next;
		delete temp;
	}
}
  • 这里用这样的一张图:我们只需要同时更新前后的指针跳过需要删除的元素,然后再对需要删除的节点进行delete就可以了
删除 20
X 20
head
10
30
nullptr
2-3 基础功能实现
  • 老规矩先上完整代码,后面带解说(不是)
#include <iostream>

template <typename T>
class Node {
public:
    T data;           // 存储数据
    Node* next;       // 指向下一个节点

    Node(T val) : data(val), next(nullptr) {} // 构造函数
};

template <typename T>
class LinkedList {
private:
    Node<T>* head;  // 头节点指针

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

    // 头部插入
    void insertFront(T value) {
        Node<T>* newNode = new Node<T>(value);
        newNode->next = head;
        head = newNode;
    }

    // 尾部插入
    void insertBack(T value) {
        Node<T>* newNode = new Node<T>(value);
        if (!head) {
            head = newNode;
            return;
        }
        Node<T>* temp = head;
        while (temp->next) {
            temp = temp->next;
        }
        temp->next = newNode;
    }

    // 删除节点
    void remove(T value) {
        if (!head) return;
        if (head->data == value) {
            Node<T>* temp = head;
            head = head->next;
            delete temp;
            return;
        }
        Node<T>* current = head;
        while (current->next && current->next->data != value) {
            current = current->next;
        }
        if (current->next) {
            Node<T>* temp = current->next;
            current->next = current->next->next;
            delete temp;
        }
    }

    // 查找节点
    bool search(T value) {
        Node<T>* temp = head;
        while (temp) {
            if (temp->data == value) return true;
            temp = temp->next;
        }
        return false;
    }

    // 遍历链表
    void display() {
        Node<T>* temp = head;
        while (temp) {
            std::cout << temp->data << " -> ";
            temp = temp->next;
        }
        std::cout << "NULL\n";
    }

    // 析构函数(释放内存)
    ~LinkedList() {
        Node<T>* current = head;
        while (current) {
            Node<T>* next = current->next;
            delete current;
            current = next;
        }
    }
};

// 测试
int main() {
    LinkedList<int> list;
    list.insertFront(3);
    list.insertFront(2);
    list.insertFront(1);
    list.insertBack(4);
    list.insertBack(5);
    
    list.display(); // 1 -> 2 -> 3 -> 4 -> 5 -> NULL

    list.remove(3);
    list.display(); // 1 -> 2 -> 4 -> 5 -> NULL

    std::cout << "Search 4: " << (list.search(4) ? "Found" : "Not Found") << std::endl; // Found
    std::cout << "Search 6: " << (list.search(6) ? "Found" : "Not Found") << std::endl; // Not Found

    return 0;
}

  • 请添加图片描述
2-4 单向链表的优缺点

优点

  • 动态分配内存,不需要预分配固定大小。
  • 插入和删除操作时间复杂度为 O(1)(头部插入/删除),比数组快(数组的插入/删除通常涉及元素移动)。
    缺点
  • 无法直接访问某个索引(不像数组可以用索引访问),查找操作需要 O(n) 时间
  • 由于每个节点都存储了一个指针,额外的内存开销较大

3 双向链表

3-1 概念
  • 双向链表(Doubly Linked List)是一种链表结构,每个节点有两个指针:

    • next:指向下一个节点。
    • prev:指向前一个节点。
      请添加图片描述
  • 代码只比单向链表多了一个指针而已

template <typename T>
class Node {
public:
    T data;           // 存储数据
    Node* next;       // 指向下一个节点
    Node* prev;       // 指向前一个节点

    Node(T val) : data(val), next(nullptr), prev(nullptr) {} // 构造函数
};

3-2 双向链表的头部插入
  • 在插入时,更新新节点的 next 指向原来的头节点,并更新原头节点的 prev 指向新节点。
void insertFront(T value) {
    Node<T>* newNode = new Node<T>(value);
    if (head) {
        newNode->next = head;
        head->prev = newNode;
    }
    head = newNode;
}
  • l老规矩上图
插入新节点 5
next
prev
head = newNode
prev = newNode
10
5
nullptr
head
20
30
nullptr
3-3 删除元素
  • 一样的
void remove(T value) {
    if (!head) return;
    
    // 如果是头节点
    if (head->data == value) {
        Node<T>* temp = head;
        head = head->next;
        if (head) {
            head->prev = nullptr;
        }
        delete temp;
        return;
    }

    // 遍历找到节点并删除
    Node<T>* current = head;
    while (current && current->data != value) {
        current = current->next;
    }
    if (current) {
        if (current->prev) {
            current->prev->next = current->next;
        }
        if (current->next) {
            current->next->prev = current->prev;
        }
        delete current;
    }
}

删除 20
next = C.next
prev = B
X 20
head
10
30
nullptr
3-4 完整实现
  • 相比单向链表只是多了一个指针
#include <iostream>

template <typename T>
class Node {
public:
    T data;           // 存储数据
    Node* next;       // 指向下一个节点
    Node* prev;       // 指向前一个节点

    Node(T val) : data(val), next(nullptr), prev(nullptr) {} // 构造函数
};

template <typename T>
class DoublyLinkedList {
private:
    Node<T>* head;  // 头节点指针

public:
    DoublyLinkedList() : head(nullptr) {} // 构造函数

    // 头部插入
    void insertFront(T value) {
        Node<T>* newNode = new Node<T>(value);
        if (head) {
            newNode->next = head;
            head->prev = newNode;
        }
        head = newNode;
    }

    // 尾部插入
    void insertBack(T value) {
        Node<T>* newNode = new Node<T>(value);
        if (!head) {
            head = newNode;
            return;
        }
        Node<T>* temp = head;
        while (temp->next) {
            temp = temp->next;
        }
        temp->next = newNode;
        newNode->prev = temp;
    }

    // 删除节点
    void remove(T value) {
        if (!head) return;

        // 如果是头节点
        if (head->data == value) {
            Node<T>* temp = head;
            head = head->next;
            if (head) {
                head->prev = nullptr;
            }
            delete temp;
            return;
        }

        // 遍历找到节点并删除
        Node<T>* current = head;
        while (current && current->data != value) {
            current = current->next;
        }
        if (current) {
            if (current->prev) {
                current->prev->next = current->next;
            }
            if (current->next) {
                current->next->prev = current->prev;
            }
            delete current;
        }
    }

    // 查找节点
    bool search(T value) {
        Node<T>* temp = head;
        while (temp) {
            if (temp->data == value) return true;
            temp = temp->next;
        }
        return false;
    }

    // 遍历链表
    void display() {
        Node<T>* temp = head;
        while (temp) {
            std::cout << temp->data << " <=> ";
            temp = temp->next;
        }
        std::cout << "NULL\n";
    }

    // 析构函数(释放内存)
    ~DoublyLinkedList() {
        Node<T>* current = head;
        while (current) {
            Node<T>* next = current->next;
            delete current;
            current = next;
        }
    }
};

// 测试
int main() {
    DoublyLinkedList<int> list;
    
    // 头部插入
    list.insertFront(3);
    list.insertFront(2);
    list.insertFront(1);
    
    // 尾部插入
    list.insertBack(4);
    list.insertBack(5);

    // 显示链表
    list.display(); // 1 <=> 2 <=> 3 <=> 4 <=> 5 <=> NULL

    // 删除节点
    list.remove(3);
    list.display(); // 1 <=> 2 <=> 4 <=> 5 <=> NULL

    // 查找节点
    std::cout << "Search 4: " << (list.search(4) ? "Found" : "Not Found") << std::endl; // Found
    std::cout << "Search 6: " << (list.search(6) ? "Found" : "Not Found") << std::endl; // Not Found

    return 0;
}

  • 请添加图片描述
3-4 双向链表的优缺点

优点

  • 双向遍历:每个节点不仅指向下一个节点,还指向前一个节点,可以在两端进行遍历,提升了某些操作的效率。
  • 高效的插入/删除:由于可以通过前向指针和后向指针直接访问相邻节点,因此在链表中间进行插入或删除时,时间复杂度为 O(1)(只要我们有指向该节点的指针),比单向链表更高效。
  • 支持双向遍历:可以轻松从链表的尾部向头部遍历,适合某些需要双向遍历的数据结构(如浏览器历史记录、音乐播放器的播放列表等)。
    缺点
  • 额外的内存开销:每个节点不仅需要存储指向下一个节点的指针,还需要存储指向前一个节点的指针,因此每个节点需要额外的空间,增加了内存开销。
  • 操作复杂性:插入和删除时需要同时更新前向和后向指针,操作比单向链表稍显复杂,代码实现也更加繁琐。
  • 访问速度依然较慢:虽然支持双向遍历,但仍然需要 O(n) 时间才能找到指定位置的节点,访问速度不如数组的 O(1)

4 循环链表(Circular Linked List)

4-1 概念
  • 循环链表是一种特殊类型的链表,其中最后一个节点指向第一个节点,形成一个环状结构。与普通链表不同,循环链表的尾节点不指向 nullptr,而是指向头节点,使得链表中的节点可以循环遍历。
4-2 特点以及注意点
  • 环形结构:尾节点指向头节点,使得链表可以无限循环遍历。
  • 没有终止条件:在普通链表中,遍历到尾节点时会遇到 nullptr但在循环链表中,遍历是一个无限循环的过程,因此需要特殊的终止条件来避免死循环。
4-3 单向循环链表的头插法
  • 同样,循环链表可以是双向链表或者单向链表,这里我们实现单向循环链表
// 插入元素到循环链表尾部
    void insertBack(T value) {
        Node<T>* newNode = new Node<T>(value);
        if (!head) {
            head = newNode;
            newNode->next = head;  // 新节点指向头节点,形成环形结构
            return;
        }
        Node<T>* temp = head;
        while (temp->next != head) {  // 遍历到尾节点
            temp = temp->next;
        }
        temp->next = newNode;   // 尾节点的next指向新节点
        newNode->next = head;   // 新节点的next指向头节点,形成环形
    }
插入新节点 5
next
head = newNode
10
5
head
20
30
4-4 删除
// 删除节点
    void remove(T value) {
        if (!head) return;
        if (head->data == value) {  // 删除头节点
            Node<T>* temp = head;
            if (head->next == head) {  // 只有一个节点
                delete head;
                head = nullptr;
            } else {
                Node<T>* temp2 = head;
                while (temp2->next != head) {
                    temp2 = temp2->next;
                }
                head = head->next;  // 更新头节点
                temp2->next = head;  // 尾节点的next指向新头节点
                delete temp;
            }
            return;
        }
        Node<T>* current = head;
        while (current->next != head && current->next->data != value) {
            current = current->next;
        }
        if (current->next != head) {
            Node<T>* temp = current->next;
            current->next = current->next->next;
            delete temp;
        }
    }
删除 20
next = C.next
next = B
X 20
head
10
30
4-5 遍历
  • 上述说到了循环链表没有终止条件:在普通链表中,遍历到尾节点时会遇到 nullptr但在循环链表中,遍历是一个无限循环的过程,因此需要特殊的终止条件来避免死循环。
  • 为此我们使用一个巧妙的方式来记录一开始遍历的位置,再次遍历到这个位置的时候停止循环:
 // 遍历循环链表
    void display() {
        if (!head) return;
        Node<T>* temp = head;
        do {
            std::cout << temp->data << " -> ";
            temp = temp->next;
        } while (temp != head);  // 遍历回到头节点时停止
        std::cout << "(head)\n";
    }
  • 同样手法我们实现于析构:
 ~CircularLinkedList() {
        if (!head) return;
        Node<T>* current = head;
        do {
            Node<T>* temp = current;
            current = current->next;
            delete temp;
        } while (current != head);  // 循环直到回到头节点
    }
4-5 完整实现
#include <iostream>

template <typename T>
class Node {
public:
    T data;           // 存储数据
    Node* next;       // 指向下一个节点

    Node(T val) : data(val), next(nullptr) {} // 构造函数
};

template <typename T>
class CircularLinkedList {
private:
    Node<T>* head;  // 头节点指针

public:
    CircularLinkedList() : head(nullptr) {}

    // 插入元素到循环链表尾部
    void insertBack(T value) {
        Node<T>* newNode = new Node<T>(value);
        if (!head) {
            head = newNode;
            newNode->next = head;  // 新节点指向头节点,形成环形结构
            return;
        }
        Node<T>* temp = head;
        while (temp->next != head) {  // 遍历到尾节点
            temp = temp->next;
        }
        temp->next = newNode;   // 尾节点的next指向新节点
        newNode->next = head;   // 新节点的next指向头节点,形成环形
    }

    // 遍历循环链表
    void display() {
        if (!head) return;
        Node<T>* temp = head;
        do {
            std::cout << temp->data << " -> ";
            temp = temp->next;
        } while (temp != head);  // 遍历回到头节点时停止
        std::cout << "(head)\n";
    }

    // 删除节点
    void remove(T value) {
        if (!head) return;
        if (head->data == value) {  // 删除头节点
            Node<T>* temp = head;
            if (head->next == head) {  // 只有一个节点
                delete head;
                head = nullptr;
            } else {
                Node<T>* temp2 = head;
                while (temp2->next != head) {
                    temp2 = temp2->next;
                }
                head = head->next;  // 更新头节点
                temp2->next = head;  // 尾节点的next指向新头节点
                delete temp;
            }
            return;
        }
        Node<T>* current = head;
        while (current->next != head && current->next->data != value) {
            current = current->next;
        }
        if (current->next != head) {
            Node<T>* temp = current->next;
            current->next = current->next->next;
            delete temp;
        }
    }

    ~CircularLinkedList() {
        if (!head) return;
        Node<T>* current = head;
        do {
            Node<T>* temp = current;
            current = current->next;
            delete temp;
        } while (current != head);  // 循环直到回到头节点
    }
};

// 测试
int main() {
    CircularLinkedList<int> list;
    list.insertBack(10);
    list.insertBack(20);
    list.insertBack(30);
    list.insertBack(40);

    list.display();  // 10 -> 20 -> 30 -> 40 -> (head)

    list.remove(30);
    list.display();  // 10 -> 20 -> 40 -> (head)

    return 0;
}

  • 请添加图片描述

4-6 循环链表的优缺点

优点

  • 无空闲尾节点:在循环链表中,尾节点始终指向头节点,链表形成一个环,无论从头部还是尾部开始遍历,都可以回到起点。这特性使得它适用于一些特定场景,如 循环缓冲区任务调度
  • 高效的环形操作:当需要循环操作时,循环链表提供了高效的解决方案,例如在操作系统的调度算法中,循环链表能够持续循环处理任务。
  • 固定内存管理:由于循环链表不依赖于标志性的 nullptr(空指针),因此可以更简单地实现某些功能,比如固定大小的缓冲区,而不必在末尾节点处理额外逻辑。
    缺点
  • 不能表示空链表:循环链表的末尾总是指向头节点,因此无法通过判断尾节点是否为 nullptr 来确定链表是否为空。需要额外的标记或逻辑来判断链表是否为空。
  • 操作复杂:由于每个节点指向下一个节点,并且尾节点指向头节点,因此需要特别注意尾节点与头节点的连接关系,插入和删除操作较为复杂。
  • 容易产生无限循环:不小心操作错误时,例如在遍历时没有适当的终止条件,可能会导致程序进入无限循环,难以避免这种情况。
  • 内存开销:与单向链表类似,每个节点需要存储一个额外的指针来指向下一个节点,在循环链表中仍然会增加内存的使用,尤其是在大型数据结构时,内存开销不容忽视。


5 跳表(Skip List)(严格单调)

  • 上述链表是不是看腻了?那来看看跳表吧!)
5-1 概念
  • 跳表(Skip List)是一种基于链表的数据结构,它通过在每一层上创建额外的“跳跃”指针来加速查找过程,从而达到类似于 平衡树 的效果。
  • 跳表通过多层次的链表结构来减少查找的时间复杂度,从而在某些场景中提供更高效的搜索操作。
5-2 结构
  • 跳表是由多个层次的链表组成,每一层都是一个有序链表。底层链表存储所有元素,而每一层以上的链表都只存储一些随机选择的元素,层次越高,元素越少。
    • 底层链表:包含所有元素,具有 O(n) 的查找复杂度。
    • 上层链表:每一层仅包含上一层链表的部分元素,且元素是从底层链表中挑选出来的,==通常通过随机方式决定哪些元素进入该层。==通过在多个层次之间跳跃,可以大大减少查找的范围,提升查找效率。
  • 没听懂?不慌来个图:
Level 0
Level 1
Level 2
Level 3
14
10
9
8
6
5
3
2
1
14
10
8
5
2
1
14
8
5
2
14
8
2
5-3 为什么跳表每一层必须是有序的
  • 如果跳表的某一层顺序是乱的,那么它就无法实现快速查找的功能。
  • 我们来看一个 跳表的正确示例
Level 2:      head → 2 → 6 → nullptr
Level 1:      head → 2 → 4 → 6 → nullptr
Level 0:      head → 2 → 3 → 4 → 5 → 6 → nullptr
  • 每一层都是从小到大排序的,所以当我们查找 5 时:
    • Level 2 直接跳到 6,发现 5 < 6,所以回退到 Level 1。
    • Level 1 直接跳到 4,再往前走发现 5
    • 这样,我们很快就能找到 5
      跳表的每一层必须是有序的(从小到大)。
      如果某一层无序,跳表的查询、插入、删除都无法正确工作!
5-4 分类
  • 是的兄弟,跳表(Skip List) 也可以分为 单向跳表双向跳表,主要区别在于 节点是否存储前驱指针(prev)
  • 这里我们还是以单向跳表为主来说明

5-5 节点定义
  • 不同去前面几个链表,跳表的每一个节点需要存储这个节点再各个层的指针(以单向跳表为例子,就是存储每一层指向下一个位置节点的指针)
template <typename T>
class Node {
public:
    T data;
    std::vector<Node*> forward;  // 存储跳表中各层的指针

    Node(T value, int level) : data(value), forward(level, nullptr) {}
};
5-6 跳表类
  • 这里我们需要指定一个跳表的最大层数和跳表升层的概率
template <typename T>
class SkipList {
private:
    Node<T>* head; // 头节点
    int maxLevel;  // 最大层数
    float probability; // 控制节点升层概率

5-7 update数组
  • 在上述代码(删除和添加函数)中,你会看到一个数组的影子:
    • 这就是一个存储 Node<T>* 的数组,每个位置都初始化为 nullptr
std::vector<Node<T>*> update(maxLevel, nullptr);
  • 那么它有什么用?我们直接来看头插和删除:
5-7 头插
  • 有点长但是不慌我们一个个看
private:
    // 随机生成跳表的层数
    int randomLevel() {
        int level = 1;
        while (rand() / float(RAND_MAX) < probability && level < maxLevel) {
            ++level;
        }
        return level;
    }
// 插入元素
void insert(T value) {
	std::vector<Node<T>*> update(maxLevel, nullptr);
	Node<T>* current = head;
	for (int i = maxLevel - 1; i >= 0; --i) {
		while (current->forward[i] && current->forward[i]->data < value) {
			current = current->forward[i];
		}
		update[i] = current;
	}

	current = current->forward[0];
	if (current == nullptr || current->data != value) {
		
		int level = randomLevel();
		
		Node<T>* newNode = new Node<T>(value, level);
		for (int i = 0; i < level; ++i) {
			newNode->forward[i] = update[i]->forward[i];
			update[i]->forward[i] = newNode;
		}
	}
}

  • 看到代码是不是吓死了(不是),不慌我们还是一个例子来看看:
5-6-2 例子
  • 假设当前跳表如下(只展示 forward[0]):
head → [2] → [4] → [6] → [8] → nullptr
  • 现在,我们想插入 5
5-6-3 遍历跳表,找到 5 应该插入的位置
std::vector<Node<T>*> update(maxLevel, nullptr);
	Node<T>* current = head;
	for (int i = maxLevel - 1; i >= 0; --i) {
		while (current->forward[i] && current->forward[i]->data < value) {
			current = current->forward[i];
		}
		update[i] = current;
	}
  1. 从最高层(Level 2)向下遍历,每一层找到 5 之前的最大值
  2. 记录这些前驱节点到 update 数组中,方便稍后修改指针。
层级跳表内容update 记录前驱
Level 2head → 2 → 6 → nullptrupdate[2] = 2
Level 1head → 2 → 4 → 6 → nullptrupdate[1] = 4
Level 0head → 2 → 4 → 6 → 8 → nullptrupdate[0] = 4
5-6-4 插入新节点
  • 这一部一样
Node<T>* newNode = new Node<T>(value, level);
5-6-5 更新指针
  • 利用 update 数组,我们可以快速修改指针,而不需要重新遍历链表:
for (int i = 0; i < level; ++i) {
    newNode->forward[i] = update[i]->forward[i];  // 让 5 指向 6
    update[i]->forward[i] = newNode;  // 让 4 指向 5
}

指针变化如下:

层级旧的指针新的指针
Level 14 → 64 → 5 → 6
Level 04 → 64 → 5 → 6
  • 最后跳表变成:
Level 2:      head → 2 → 6 → nullptr
Level 1:      head → 2 → 4 → 5 → 6 → nullptr
Level 0:      head → 2 → 4 → 5 → 6 → 8 → nullptr


5-7 删除
  • 和插入一个逻辑,借用update
// 删除元素  
void remove(T value) {  
    std::vector<Node<T>*> update(maxLevel, nullptr);  
    Node<T>* current = head;  
    for (int i = maxLevel - 1; i >= 0; --i) {  
        while (current->forward[i] && current->forward[i]->data < value) {  
            current = current->forward[i];  
        }  
        update[i] = current;  
    }  
  
    current = current->forward[0];  
    if (current && current->data == value) {  
        for (int i = 0; i < maxLevel; ++i) {  
            if (update[i]->forward[i] != current) break;  
            update[i]->forward[i] = current->forward[i];  
        }  
        delete current;  
    }  
}
5-7-1 例子:
  • 假设我们现在想删除 4,跳表当前结构如下:
Level 2:      head → 2 → 6 → nullptr
Level 1:      head → 2 → 4 → 5 → 6 → nullptr
Level 0:      head → 2 → 4 → 5 → 6 → 8 → nullptr
5-7-2 遍历跳表,找到 4 的前驱节点
  • 目标: 记录每层4 的前驱节点,方便删除后调整指针。
层级跳表内容update 记录前驱
Level 1head → 2 → 4 → 5 → 6 → nullptrupdate[1] = 2
Level 0head → 2 → 4 → 5 → 6 → 8 → nullptrupdate[0] = 2
5-7-3 修改指针
for (int i = 0; i < maxLevel; ++i) {
    if (update[i]->forward[i] != current) break;
    update[i]->forward[i] = current->forward[i];  // 让 2 直接指向 5
}

  • 删除 4 之前的指针情况:
Level 1:      head → 2 → 4 → 5 → 6 → nullptr
Level 0:      head → 2 → 4 → 5 → 6 → 8 → nullptr
  • 删除 4 之后的指针情况:
Level 1:      head → 2 → 5 → 6 → nullptr
Level 0:      head → 2 → 5 → 6 → 8 → nullptr
5-8 完整代码
#include <iostream>  
#include <vector>  
#include <cstdlib>  
#include <ctime>  
  
template <typename T>  
class Node {  
public:  
    T data;  
    std::vector<Node*> forward;  // 存储跳表中各层的指针  
  
    Node(T value, int level) : data(value), forward(level, nullptr) {}  
};  
  
template <typename T>  
class SkipList {  
private:  
    Node<T>* head;  
    int maxLevel;  
    float probability;  
  
public:  
    SkipList(int maxLevel, float probability)  
            : maxLevel(maxLevel), probability(probability) {  
        head = new Node<T>(T(), maxLevel);  // 创建一个头节点  
        srand(time(0));  
    }  
  
    // 插入元素  
    void insert(T value) {  
        std::vector<Node<T>*> update(maxLevel, nullptr);  
        Node<T>* current = head;  
        for (int i = maxLevel - 1; i >= 0; --i) {  
            while (current->forward[i] && current->forward[i]->data < value) {  
                current = current->forward[i];  
            }  
            update[i] = current;  
        }  
  
        current = current->forward[0];  
        if (current == nullptr || current->data != value) {  
            int level = randomLevel();  
            Node<T>* newNode = new Node<T>(value, level);  
            for (int i = 0; i < level; ++i) {  
                newNode->forward[i] = update[i]->forward[i];  
                update[i]->forward[i] = newNode;  
            }  
        }  
    }  
  
    // 查找元素  
    bool search(T value) {  
        Node<T>* current = head;  
        for (int i = maxLevel - 1; i >= 0; --i) {  
            while (current->forward[i] && current->forward[i]->data < value) {  
                current = current->forward[i];  
            }  
        }  
        current = current->forward[0];  
        return current && current->data == value;  
    }  
  
    // 删除元素  
    void remove(T value) {  
        std::vector<Node<T>*> update(maxLevel, nullptr);  
        Node<T>* current = head;  
        for (int i = maxLevel - 1; i >= 0; --i) {  
            while (current->forward[i] && current->forward[i]->data < value) {  
                current = current->forward[i];  
            }  
            update[i] = current;  
        }  
  
        current = current->forward[0];  
        if (current && current->data == value) {  
            for (int i = 0; i < maxLevel; ++i) {  
                if (update[i]->forward[i] != current) break;  
                update[i]->forward[i] = current->forward[i];  
            }  
            delete current;  
        }  
    }  
  
    // 打印跳表  
    void print() {  
        for (int i = 0; i < maxLevel; ++i) {  
            Node<T>* current = head->forward[i];  
            std::cout << "Level " << i << ": ";  
            while (current) {  
                std::cout << current->data << " ";  
                current = current->forward[i];  
            }  
            std::cout << std::endl;  
        }  
    }  
  
private:  
    // 随机生成跳表的层数  
    int randomLevel() {  
        int level = 1;  
        while (rand() / float(RAND_MAX) < probability && level < maxLevel) {  
            ++level;  
        }  
        return level;  
    }  
};  
  
int main() {  
    SkipList<int> list(4, 0.5);  
  
    list.insert(3);  
    list.insert(6);  
    list.insert(7);  
    list.insert(9);  
    list.insert(12);  
    list.insert(19);  
  
    list.print();  
  
    std::cout << "Search 6: " << (list.search(6) ? "Found" : "Not Found") << std::endl;  
    list.remove(6);  
    list.print();  
    std::cout << "Search 6: " << (list.search(6) ? "Found" : "Not Found") << std::endl;  
  
    return 0;  
}
  • 在这里插入图片描述
5-9 跳表(Skip List)的优缺点

优点

  • 无空闲尾节点:在循环链表中,尾节点始终指向头节点,链表形成一个环。无论从头部还是尾部开始遍历,都可以回到起点。这一特性使得它适用于一些特定场景,如 循环缓冲区任务调度
  • 高效的环形操作:当需要循环操作时,循环链表提供了高效的解决方案。比如在操作系统的调度算法中,循环链表能够持续循环处理任务,如 时间片轮转调度
  • 固定内存管理:由于循环链表不依赖于标志性的 nullptr(空指针),因此可以更简单地实现某些功能,比如固定大小的缓冲区,而不必在末尾节点处理额外逻辑。
    缺点
  • 不能表示空链表:循环链表的末尾总是指向头节点,因此无法通过判断尾节点是否为 nullptr 来确定链表是否为空。需要额外的标记或逻辑来判断链表是否为空。
  • 操作复杂:由于每个节点指向下一个节点,并且尾节点指向头节点,因此需要特别注意尾节点与头节点的连接关系,插入和删除操作较为复杂。例如在插入或删除操作时,需要确保尾节点的指针始终指向头节点。
  • 容易产生无限循环:不小心操作错误时,例如在遍历时没有适当的终止条件,可能会导致程序进入无限循环。特别是在遍历循环链表时,没有明确的终止条件时,很容易发生这种情况。
  • 内存开销:与单向链表类似,每个节点需要存储一个额外的指针来指向下一个节点。在循环链表中仍然会增加内存的使用,尤其在处理大型数据结构时,内存开销较大。特别是在节点数目很大时,内存管理也可能变得复杂。

6 总结

  • 本文分别讲述了如何使用C++分别实现单向链表,双向链表,循环链表,跳表
  • 下个系列编程五大池我们将会用到单向链表,双向链表!
  • 如有错误,欢迎指出!!
  • 感谢大家的支持!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值