我要成为数据结构与算法高手(二)之单链表

100 阅读9分钟

1. 结点类型定义 listNode

struct listNode
{
    int data;   // 数据域
    struct listNode *next;   // 指针域
};
  • data: 存储结点的数据(这里是整数)。
  • next: 指向下一个结点的指针。如果这是链表的最后一个结点,next 的值为 NULL。

文本图示 (一个结点):

+------+------+
| data | next |
+------+------+
|  数据 |  ----->  (指向下一个结点或 NULL)
+------+------+
//定义结点类型
struct listNode
{
    int data;   //数据域
    struct listNode *next;   //指针域
};

int main()
{
	 int ar[] = {1, 2, 3, 4, 5};
    int n = sizeof(ar) / sizeof(ar[0]);
    
    return 0;
}

2. 尾部插入创建链表 createList(int ar[], int n)

  • 功能: 根据数组 ar 的内容,创建一个新的单链表。采用尾插法,新结点总是添加到链表的末尾。

  • 参数:

    • ar[]: 包含要插入链表的数据的数组。
    • n: 数组 ar 中元素的个数。
  • 图示 (以 ar = {1, 2, 3}n = 3 为例):

初始状态 (创建头结点 phead):

image.png

循环过程 (尾部插入):

i = 1

  1. 创建一个新结点 s,s->data = ar[1] (2)s->next = NULL

1.png 2. p->next = s; (将 phead 的 next 指针指向 s)

2.png 3. p = s; (移动 p 指针,使其指向新插入的结点 s)

3.png

p = s; 在下一次循环中,p 就指向了当前链表的最后一个节点,这样才能继续在末尾添加新节点

如果没有这一步,p 会一直停留在头节点上,那么每次循环都会将新节点添加到头节点之后,并覆盖之前添加的节点,最终形成的链表只会包含头节点和最后一个添加的节点,而不是所有节点按顺序连接

简而言之,就是 p 把前面的节点和后面新建的节点链接起来,起到链接的作用

i = 2

  1. 创建新结点 ss->data = ar[2] (3)s->next = NULL

4.png

  1. p->next = s;

5.png

循环结束, 返回 phead:

6.png

//尾部插入创建链表
listNode *createList(int ar[], int n)
{
    //(申请第一个结点)创建头结点
    listNode* phead = (listNode*)malloc(sizeof(listNode));
    phead->data = ar[0];
    phead->next = NULL;

    listNode* p = phead;
    for(int i = 1; i < n; i++)
    {
        listNode* s = (listNode*)malloc(sizeof(listNode));
        s->data = ar[i];
        s->next = NULL;

        p->next = s;
        p = s;  //p = p->next;
    }
    return phead;
}


int main()
{
		listNode* head = NULL;      //初始化一个空链表
    head = createList(ar, n);
}

3. 打印链表 printList(listNode* phead)

  • 功能: 从头到尾遍历链表,打印每个结点的数据域。

  • 参数:

    • phead: 指向链表头结点的指针。
  • 图示 (假设链表为 1 -> 2 -> 3 -> NULL):

//打印链表
void printList(listNode* phead) 
{
    listNode* p = phead;
    while(p != NULL)
    {
        printf("%d--> ", p->data);
        p = p->next;   //让 p 指向链表的下一个节点。
    }
    printf("NULL");
    printf("\n");
}

Q&A

Q:同样是p往后移,这里的p = p->next;为什么不能换成p++?

A:因为p 是一个指向 listNode 结构的指针,而 p++ 仅适用于数组指针或指向连续内存的指针,而链表的节点在内存中并不一定是连续存储的。

  1. 链表的节点在内存中是动态分配的,节点的位置是随机的,并不保证连续存储。

  2. p++ 只是让 p 增加 sizeof(listNode) 字节,而不是指向链表的下一个节点。

  3. 正确的前进方式是按照 p->next 指向的地址跳转,而 p++ 无法做到这一点。

由于 p 是 listNode*,p++ 实际上会将 p 移动到下一个 listNode 位置,假设 listNode 结构体大小为 sizeof(listNode),则 p++ 相当于 p = p + 1;,即增加 sizeof(listNode) 个字节,而不是沿着链表的 next 指针前进。

7.png

循环过程:

  1. p 指向 1, 打印 "1--> "
  2. p 移动到下一个结点 (2)
  3. p 指向 2, 打印 "2--> "
  4. p 移动到下一个结点 (3)
  5. p 指向 3, 打印 "3--> "
  6. p 移动到下一个结点 (NULL)
  7. p 为 NULL, 循环结束, 打印 "NULL"

输出: 1--> 2--> 3--> NULL

4. 查找结点 findNode(listNode* phead, int key)

  • 功能: 在链表中查找数据域值为 key 的结点。

  • 参数:

    • phead: 指向链表头结点的指针。
    • key: 要查找的数据值。
  • 返回值:

    • 如果找到值为 key 的结点,返回指向该结点的指针。
    • 如果未找到,返回 NULL
  • 图示 (假设链表为 1 -> 2 -> 3 -> NULL, 查找 key = 2):

//查找结点
listNode* findNode(listNode* phead, int key)    //第一种方法
{
    listNode* p = phead;
    while(p != NULL && p->data != key)
    {
        p = p->next;
    }
    return p;
}

8.png

循环过程:

  1. p 指向 1, p->data (1) != key (2)
  2. p 移动到下一个结点 (2)
  3. p 指向 2, p->data (2) == key (2)
  4. 循环结束, 返回 p (指向结点 2 的指针)

5. 在 key 结点后面插入结点 insertNodeBack(listNode* phead, int key, int x)

  • 功能: 在链表中找到数据域值为 key 的结点,并在其后面插入一个数据域值为 x 的新结点。

  • 参数:

    • phead: 指向链表头结点的指针。
    • key: 要查找的结点的数据值。
    • x: 要插入的新结点的数据值。
  • 返回值: 指向链表头结点的指针 (可能因为在头部插入而改变)。

  • 图示 (假设链表为 1 -> 2 -> 3 -> NULLkey = 2x = 5):

//在key结点后面插入一个结点x
listNode* insertNodeBack(listNode* phead, int key, int x)
{
    //查找key结点
    listNode *pos = findNode(phead, key);  //见上面第四点查找结点findNode函数
    if(pos == NULL)
        return phead;

    //创建新结点
    listNode *s = (listNode*)malloc(sizeof(listNode));
    s->data = x;

    //插入新结点
    s->next = pos->next;  //保证链接的链表不会断裂
    pos->next = s;

    return phead;
}

1. 查找 key = 2:

9.png 2. 创建新结点 s:

10.png 3. 插入新结点:

11.png 4. 插入完成(return phead):

12.png

6. 在 key 结点前面插入结点 insertNodeFront(listNode* phead, int key, int x)

  • 功能: 在链表中找到数据域值为 key 的结点,并在其前面插入一个数据域值为 x 的新结点。
  • 参数: 与 insertNodeBack 相同。
  • 图示 (假设链表 1 -> 2 -> 3 -> NULLkey = 2x = 5):
//在key结点前面插入一个结点x
listNode* insertNodeFront(listNode* phead, int key, int x)
{
    listNode *p = phead, *pre = NULL;   //pre用来记录p的前一个结点
    while(p!= NULL && p->data!= key)
    {
        pre = p;
        p = p->next;
    }
    if(p == NULL)
        return phead;
    
    //创建新结点
    listNode *s = (listNode*)malloc(sizeof(listNode));
    s->data = x;

    //插入新结点
    s->next = p;

    if(pre == NULL)   //如果key是第一个结点(在第一个结点前插入)
        phead = s;    //在第一个结点的前面插入,需要修改表头指针
    else
        pre->next = s;

    return phead;
}

1. 插入前:

13.png 2.创建结点

14.png

3.插入过程:

//插入
s->next = p;
if(pre == NULL)   //如果key是第一个结点(在第一个结点前插入)
     phead = s;    //在第一个结点的前面插入,需要修改表头指针
 else
     pre->next = s;

假设key=1, 那么pre==NULLphead直接指向新结点, 链表就变成了5->1->2->3->NULL

15.png 假设key=2, 那么pre指向1, s插入到1和2之间,链表变成了1->5->2->3->NULL

16.png

4.插入结束(return pheadkey=2的情况):

17.png

7. 删除节点 deleteNode(listNode* phead, int key)

  • 功能: 删除链表中第一个值为 key 的节点。

  • 参数:

    • phead: 链表头指针。
    • key: 要删除的节点的值。
  • 图示 (假设链表为 1 -> 5 -> 2 -> 3 -> NULLkey = 5):

//删除结点
listNode* deleteNode(listNode* phead, int key)
{
    //查找key结点
    listNode *p = phead, *pre = NULL;   //pre用来记录p的前一个结点,前驱结点
    while(p!= NULL && p->data!= key)
    {
        pre = p;
        p = p->next;
    }
    if(p == NULL)
        return phead;

    //删除结点
    if(pre == NULL)   //情况 1:如果前驱结点是空的(删除第一个结点)
        phead = p->next;    //删除第一个结点,需要修改表头指针
    else              //情况 2:要删除的不是第一个节点
        pre->next = p->next; //前一个节点跳过 p,指向 p 的下一个节点
}
    free(p);
    return phead;
}

1.删除前:

18.png

2.删除过程:

情况1 (删除第一个节点, key = 1):

pre == NULLphead 直接指向原链表的第二个节点, 原第一个节点被跳过

19.png 情况2(删除的不是第一个节点, key=5):

pre 指向 1, p 指向 5, pre->next 直接指向 p->next (2), 5 被跳过

20.png

3.删除后 (key = 5 的情况):

21.png

8. 反转链表 reverseList(listNode* phead)

  • 功能: 将链表中的节点顺序反转(就地反转)。

  • 参数:

    • phead: 链表头指针。
  • 图示 (假设链表为 1 -> 2 -> 3 -> 4 -> NULL):

//反转链表
listNode* reverseList(listNode* phead)
{
    //断开链表
    listNode *p = phead->next;
    phead->next = NULL;
    //反转链表
    while(p!= NULL)    //将剩余结点摘除头插
    {
        listNode* q = p->next;

        p->next = phead;
        phead = p;

        p = q;
    }
    return phead;
}

1.反转前

22.png

2.反转过程 (头插法):

23.png

24.png

加入q点的作用是保存后面的链表,因为p点移到plead前面,如果没有q点保存后面的链表,后面的链表就会丢失

25.png

修改头指针指向最新的头结点(p点)

26.png

循环反复,如此类推,知道p点为空,链表全部反转完成

3.反转完成

27.png

9. 排序链表  sortList(listNode* phead) - 使用直接插入排序

  • 功能: 将链表中的节点按数据域的值升序排列 (使用插入排序)。

  • 参数:

    • phead: 链表头指针。
  • 图示 (假设链表为 4 -> 2 -> 1 -> 3 -> NULL):

//排序链表
listNode* sortList(listNode* phead)
{
    //断开链表
    listNode *p = phead->next;
    phead->next = NULL;
    //摘除剩余链表结点按位置插入
    while(p!= NULL)    //将剩余结点摘除头插
    {
        listNode* q = p->next;

        listNode* cur = phead, *pre = NULL;     //cur用来记录p的后一个结点,前驱结点, pre用来记录cur的前一个结点
        while(cur!= NULL && cur->data < p->data)    //查找插入位置
        {
            pre = cur;
            cur = cur->next;
        }
        //插入p

        if(pre == NULL) //是否在第一个结点前插入
            phead = p;
        else
            pre->next = p;

        p->next = cur;
        p = q;   //更新结点
    }
    return phead;
}

1.排序前:

28.png

2.排序过程(步骤分解):

29.png

定义cur指向当前头指针,pre指向cur的前驱,一开始为NULL,这两个指针拿来两两比较

第一趟循环:

30.png

while(cur!= NULL && cur->data < p->data)

p点与cur对比,cur = 4 > p = 2,循环不执行跳过

31.png

if(pre == NULL)成立,执行phead = p

32.png

33.png

34.png

35.png 第二趟循环:

36.png

while(cur!= NULL && cur->data < p->data)

p点与cur对比,cur = 2 > p = 1,循环不执行跳过

37.png

if(pre == NULL)成立,执行phead = p

38.png

39.png

40.png

41.png 第三趟循环:

42.png

while(cur!= NULL && cur->data < p->data)

p点与cur对比,cur = 1 > p = 3,循环执行

43.png

44.png

走到下面这一步,循环触发了终止条件:cur->data < p->data ,cur = 4 > p = 3

45.png

if(pre == NULL) 不成立,执行else下面的语句pre->next = p;

46.png

47.png

48.png 第四趟循环:

49.png

pNULL,循环不执行,至此排序结束

3.排序后:

50.png