3.1.2【有头单向链表】

119 阅读7分钟

1. 概念

链表就是将线性表的每个结点,用指针串起来,而非直接访问地址方法。

2. 接口实现

2.1. 定义 任意结点 的结构体

typedef int LLDataType;
typedef struct LinkListNode
{
    LLDataType data;           //数据域
    struct LinkListNode *next; //指针域
} LLN, *LL;

LLNstruct LinkListNode

LLstruct LinkListNode *

LLLLN *

2.2. 创建 空的 有头单向链表(即只有头结点)

  • 想要使用 有头单向链表,需要先创建并初始化一个头结点
  • 需要 同链表结构体类型 的指针,指向头结点的地址,即头指针

2.3. 插入数据 到指定下标位置

注意:这里下标从0开始,需要手动遍历到指定位置

问题:向插入post位置,插入结点

本图思路: post - 1结点身后,尾插

  1. 先获取post位置前 的结点地址,ph先指向post - 1的结点
  2. pnew指向生成的新结点地址
  3. 将新结点,与原post位置结点,和post - 1位置结点 链接

练习:

一个单链表LinkList中的结点PH后面插入一个结点X下列操作正确的是( )

  • PH->next = PX->next; PH->next = PX;
  • PX->next = PH->next; PH->next = PX;
  • PH->next = PX; PX->next = PH->next;
  • PH->next = PX; PH->next = PX->next;

第一步pnew链接post - 1的身后

第二部post - 1链接pnew

2.4. 求链表长度(长度 即 有效元素个数)

int LLLength(LL H)
{
    int len = 0;
    
    // 头结点,不算长度内,不算有效数据节点
    while (H->next != NULL)
    {
        H = H->next;
        len++;
    }

    return len;
}

2.5. 打印 遍历

值传递的头指针,当其指针域不空,就遍历身后,然后 后移。

2.6. 判断 单向有头链表 是否为空(即仅有 头结点 吗)

头结点的指针域,若为空 则表空 返回真,否则假 返回0

2.7. 查找 指定数据 首次出现的下标位置

逐个遍历的同时,进行匹配

2.8. 修改 指定位置的 数据值

对比插入:

  • 插入用的,head 指向 post-1 位置
  • 修改用的,head 指向 post 位置
  • 删除,也是先移动到post-1位置,再用pdel指向post位置

2.9. 删除 指定位置的结点

思路

  1. post容错判断
  2. 移动head 指向 post-1 位置;
  3. 用pdel指向post位置,即 pdel = head->next
  4. 链接pdel 的 前驱 与 后继
  5. 释放被删除的 结点

对比 顺序表的删除

在已知 被删除结点的确切位置 的情况下,链表仅需要删除释放目标结点 重新建立链接,而不需要 移动部分元素。

2.10. 删除 指定数据 的所有结点

思路:在当前结点,侦测其身后结点,而不是指向目标结点后再判断

2.11. 清空 所有数据结点 只剩头结点

头结点的指针域不为空,就删除其后面所有

2.12. 转置 有头单向链表

把 无头链表 的 结点,依次插入到 头结点head 身后

3. 思路总结

  1. 链表插入删除方便,顺序表插入删除需要移动元素。
  2. 链表访问元素比较麻烦,需要遍历,顺序表查找方便因为内存连续,通过下标即可查找。
  3. 顺序表长度固定,链表长度不固定。

4. 总体代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DEBUG(Str) printf("%s %s %s %d\n", Str, __func__, __FILE__, __LINE__);

// 1. 定义用来操作有头单向链表的结构体(单向链表结点)
typedef int LLDataType;
typedef struct LinkListNode
{
    LLDataType data;           //数据域
    struct LinkListNode *next; //指针域
} LLN, *LL;

// 2. 创建 空的 有头单向链表(即只有头结点)
LL LLInit(void)
{
    // 开辟头结点
    LL H = (LL)malloc(sizeof(LLN));
    if (H == NULL)
    {
        printf("\n");
        return NULL;
    }

    //初始化头结点,并返回其堆地址
    H->next = NULL;
    return H;
}

// 3. 计算 长度 即有效元素个数
int LLLength(LL H)
{
    int len = 0;

    // 头结点,不算长度内,不算有效数据节点
    while (H->next != NULL)
    {
        H = H->next;
        len++;
    }

    return len;
}

// 4. 判断 单向有头链表 是否为空(即仅有 头结点)
int LLisEmpty(LL H)
{
    return H->next == NULL ? 1 : 0;
}

// 5. 插入数据 到指定下标位置
// 在post-1位置 尾部插入
int LLInsert(LL H, int post, LLDataType data)
{
    // 1. 容错判断[0, 长度len]
    // 新位置即len 即长度,可以等于,不可大于
    if (post < 0 || post > LLLength(H))
    {
        DEBUG("insert post err");
        return -1;
    }

    // 2. 移动头指针使其指向post前一个结点,
    // 否则无修改其前驱结点的next指针域
    // i不能=post,不然就指向了post位置结点
    for (int i = 0; i < post; i++)
        H = H->next;

    // 3. 开辟新结点存放数据
    LL PNew = (LL)malloc(sizeof(LLN));
    if (NULL == PNew)
    {
        DEBUG("new node malloc err");
        return -1;
    }
    //赋值初始化
    PNew->data = data;
    PNew->next = NULL; //最好如此,保险

    PNew->next = H->next; //新node 指向 旧post结点
    H->next = PNew;       //post-1 结点 指向 新node

    return 0;
}

// 6. 打印 遍历
void LLPrint(LL H)
{
    if (LLisEmpty(H))
    {
        DEBUG("empty linklist, can't print!");
        return;
    }

    // 头结点next不空,就打印并后挪
    while (H->next != NULL)
    {
        H = H->next;
        printf("%d\t", H->data);
    }

    putchar(10);
}

// 7. 查找 指定数据 首次出现的下标位置
int LLFind(LL H, LLDataType data)
{
    // 若存在,则下标从0开始
    int post = 0;

    while (H->next != NULL)
    {
        H = H->next;

        // 上回的post即当前位置
        if (H->data == data)
            return post;
        else
            post++;
    }

    return -1;
}

// 8. 修改 指定位置的 数据值
int LLModify(LL H, int post, LLDataType data)
{
    // 容错判断:有效位置:[0, 长度len)
    // 注意:len是长度,比下标大1
    // 所以post 不可以等于 len
    if (post < 0 || post >= LLLength(H))
    {
        DEBUG("err post to Modify");
        return -1;
    }

    // 遍历将头指针H指向post结点
    for (int i = 0; i <= post; i++)
        H = H->next;

    // 修改
    H->data = data;

    return 0;
}

// 9.删除 指定位置的结点
int LLDeletePost(LL H, int post)
{
    // 容错判断 [0, len)
    // 长度len是不存在的,不可删除
    if (post < 0 || post >= LLLength(H))
    {
        DEBUG("err post to del");
        return -1;
    }

    // 遍历,将头指针指向删除结点的前驱
    // 需要将删除结点前驱的指针域next指向删除结点后继
    for (int i = 0; i < post; i++)
        H = H->next;

    // 定义用来删除结点的指针PDel指向被删除的结点,但是不能直接对其释放,否则丢失其后继信息。
    LL PDel = H->next;

    // 被删除结点 前驱的指针域next指向删除结点后继
    // H->next = H->next->next;
    H->next = PDel->next;

    // 释放被删除的结点
    free(PDel);
    PDel = NULL;

    return 0;
}

// 10.删除 指定数据 的所有结点
void LLDeleteData(LL H, LLDataType data)
{
    LL PDel = NULL; // 用于指向被删除结点的一个指针。

    while (H->next) // 等价 H->next != NULL
    {
        if (H->next->data == data)
        {
            PDel = H->next;          //指向被删除的结点
            H->next = H->next->next; //跳过被删除结点
            free(PDel);              //释放被删除结点
            PDel = NULL;             //指针安全化
        }
        else
            H = H->next; //移动到下一个,继续
    }
}

// 11.清空 所有数据结点 只剩头结点
void LLClear(LL H)
{
    LL PDel = NULL;
    while (H->next != NULL) //身后若有则继续
    {
        PDel = H->next;       //指向被删除的结点
        H->next = PDel->next; //跳过,链接到被删除结点的后继

        free(PDel);
        PDel = NULL;
    }
}

// 12.转置 有头单向链表
// {头结点} 11 12 13 14 15 ==>
// {头结点} 15 14 13 12 11
void LLInvert(LL H)
{
    // 断开 有头单向链表,断开前需保存头结点,和其后继
    // 将其分成 空的有头单向链表 和 无头单向链表

    LL P = H->next; //无头单向链表
    H->next = NULL; //空的有头单向链表

    // 遍历无头单向链表,将每个结点 头插 进 有头单向链表中
    while (P != NULL)
    {
        // 保存无头单向链表中,将要头插结点的 后继的地址
        LL T = P->next; //用来下次成为新的头结点(无头链表中)

        // 头插
        P->next = H->next; //链接 头结点的后继
        H->next = P;       //成为head的直接后继

        // P重新指向无头单向链表的新的起始结点
        P = T; //T是临时变量
    }
}
int main(int argc, char *argv[])
{
    // 创建并初始化,头结点
    LL head = LLInit();

    // 判断有头单向链表是否空
    LLisEmpty(head) ? puts("LL Empty") : puts("LL not Empty");

    // 指定位置 插入数据
    for (int i = 0; i < 5; i++)
        LLInsert(head, i, i * 11 + 1);

    // 打印 遍历
    LLPrint(head);

    // 查看 指定数据 第一次出现的下标位置
    printf("%d at post:%d\n", 34, LLFind(head, 34));

    // 修改 指定位置 的数据值
    LLModify(head, 3, 3333);
    LLPrint(head);

    // 删除 指定下标位置 的结点
    LLDeletePost(head, 3);
    LLPrint(head);

    // 删除 所有该数据的结点
    LLDeleteData(head, 23);
    LLPrint(head);

    // 有头单向链表的转置
    LLInvert(head);
    LLPrint(head);

    // 清空链表
    LLClear(head);
    LLPrint(head);

    return 0;
}