数据结构(2)

链表的引入与概念

链表是一种物理存储结构上非连续、非顺序 的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

生活中的例子:淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉/加上,不会影响其他车厢,每节车厢都是独立存在的。

2. 单链表(Singly Linked List)

2.1 结点(Node)

链表里的每节"车厢"都是独立申请下来的空间,我们称之为"结点"。

结点的组成主要有两个部分:当前结点要保存的数据 和 保存下一个结点的地址(指针变量)。

2.2 链表的性质

  1. 链式结构在逻辑上是连续的,在物理结构上不一定连续。

  2. 结点一般是从堆上申请的。

  3. 从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,可能不连续。

cpp 复制代码
// 假设当前保存的结点为整型
struct SListNode
{
    int data;               // 结点数据
    struct SListNode* next; // 指针变量用于保存下一个结点的地址
};

2.3 链表的打印

给定链表结构,如何实现结点从头到尾的打印?

cpp 复制代码
void SLTPrint(SLTNode* phead) {
    SLTNode *pcur = phead;
    while (pcur) {
        printf("%d ", pcur->data);
        pcur = pcur->next;
    }
    printf("\n");
}

2.4 单链表的实现(SList.h)

cpp 复制代码
typedef int SLTDataType;
typedef struct SListNode
{
    SLTDataType data;       // 结点数据
    struct SListNode* next; // 指针保存下一个结点的地址
} SLTNode;

void SLTPrint(SLTNode* phead);

// 头部插入删除/尾部插入删除
void SLTPushBack(SLTNode* phead, SLTDataType x);
void SLTPushFront(SLTNode* phead, SLTDataType x);
void SLTPopBack(SLTNode* phead);
void SLTPopFront(SLTNode* phead);

// 查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
// 在指定位置之前插入数据
void SLTInsert(SLTNode* phead, SLTNode* pos, SLTDataType x);
// 删除pos结点
void SLTErase(SLTNode* phead, SLTNode* pos);
// 在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
// 删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
// 销毁链表
void SListDestroy(SLTNode* phead);

3. 链表的分类(8种结构)

链表的结构非常多样,以下情况组合起来就有8种(2×2×2)链表结构:

  • 单向 或者 双向

  • 带头 或者 不带头

  • 循环 或者 不循环

实际中最常用的是两种结构:

  1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等。在笔试面试中出现很多。

  2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。

4. 经典算法题:快慢指针(环形链表)

4.1 环形链表 I:判断是否有环

快慢指针:慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行。如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。

思考1:为什么快指针每次走两步,慢指针走一步可以相遇?

假设slow入环时,fast和slow之间的距离为N。每追击一次,距离缩小1步(N, N-1, N-2 ... 1, 0),最终距离一定会变为0,即追上。

思考2:快指针一次走3步,走4步,...n步行吗?

以走3步为例:每追击一次,距离缩小2步。

  1. 如果N是偶数,第一轮就追上了。

  2. 如果N是奇数,第一轮追不上,错过了,距离变成 C-1(C为环周长),进入新一轮追击。

    • 若 C-1 是偶数,下一轮追上。

    • 若 C-1 是奇数,永远追不上。

      结论 :追不上的前提是 N是奇数,C是偶数 。但在数学推导中(如 2L = (x+1)C - N,左边为偶数,若N为奇数,则C必须为奇数),该前提不成立,因此快指针走3步、4步最终也一定可以相遇。实际写代码时,习惯用走2步的方式。

4.2 环形链表 II:寻找入环的第一个结点

结论:让一个指针从链表起始位置开始遍历,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会入口点的位置相遇。

证明 :

设环长为R,H到入口E距离为L,E到相遇点M距离为X。

  • 相遇时,slow走的路程:L + X

  • 相遇时,fast走的路程:L + X + nR(n为fast在环内转的圈数)

  • 因为fast速度是slow的2倍,所以:2(L + X) = L + X + nR

  • 化简得:L = nR - X = (n-1)R + (R-X)

  • 当 n=1 时,L = R - X。即:从头结点出发的指针和从相遇点出发的指针,每次走一步,必定在入口点相遇。

cpp 复制代码
// 判断环并找入口代码示例
typedef struct ListNode ListNode;
ListNode *detectCycle(ListNode *head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            ListNode *ptr1 = head;
            ListNode *ptr2 = slow;
            while (ptr1 != ptr2) {
                ptr1 = ptr1->next;
                ptr2 = ptr2->next;
            }
            return ptr1;
        }
    }
    return NULL;
}

5. 带头双向循环链表(Doubly Linked List)

"带头"链表里的头结点,实际为**"哨兵位"**。哨兵位结点不存储任何有效元素,只是站在这里"放哨的"。

cpp 复制代码
typedef int LTDataType;
typedef struct ListNode
{
    struct ListNode* next; // 指针保存下一个结点的地址
    struct ListNode* prev; // 指针保存前一个结点的地址
    LTDataType data;
} LTNode;

LTNode* LTInit();
void LTDestroy(LTNode* pthread);
void LTPrint(LTNode* pthread);
bool LTEmpty(LTNode* pthread);

void LTPushBack(LTNode* pthread, LTDataType x);
void LTPopBack(LTNode* pthread);
void LTPushFront(LTNode* pthread, LTDataType x);
void LTPopFront(LTNode* pthread);
// 在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
void LTErase(LTNode* pos);
LTNode* LTFind(LTNode* pthread, LTDataType x);

6. 顺序表与链表的对比总结

不同点 顺序表 链表(单链表)
存储空间上 物理上一定连续 逻辑上连续,但物理上不一定连续
随机访问 支持:O(1) 不支持:O(N)
任意位置插入或删除元素 可能需要搬移元素,效率低 O(N) 只需修改指针指向
插入 动态顺序表,空间不够时需要扩容和空间浪费 没有容量的概念,按需申请释放,不存在空间浪费
应用场景 元素高效存储 + 频繁访问 任意位置高效插入和删除
相关推荐
D e re k ZH2 小时前
C++ 标准库算法全景指南
数据结构·c++·算法
冻柠檬飞冰走茶3 小时前
字母移动游戏题解:贪心求最长可保留连续子串(附 C 实现)
c语言·开发语言·数据结构·算法·动态规划
十二1 天前
【洛谷题解】P1083 [NOIP 2012 提高组] 借教室(二分答案 + 差分)
数据结构·c++·算法·蓝桥杯
D e re k ZH1 天前
C++ 从零入门(一):基础语法与程序结构
数据结构·c++·算法
careathers1 天前
【数据结构】基于比较的排序算法 - 交换排序
数据结构·算法·排序算法
H.莓飛1 天前
【C++】【STL】vector_OJ题
linux·服务器·开发语言·数据结构·c++·visual studio
老师我太想进步了20262 天前
【数据结构-4】算法美学之顺序表
数据结构·算法
GrowingIO技术社区2 天前
CDP、行为分析、归因与营销自动化有什么区别?
数据结构·数据挖掘·数据分析
GrowingIO技术社区2 天前
实时标签怎样验收?从数据更新到分群可用
数据结构·数据挖掘·数据分析