3.1.3【有头 双向(不循环)链表】

193 阅读7分钟

1. 概念

双向链表即:可以从前往后或者从后往前找,这就意味着链表的结点就不能只有一个指针域next了,还需要一个指向前期结点的指针域prior

头指针 指向 头结点,尾指针 指向 末个 有效结点。

  1. 逻辑结构:线性结构
  2. 存储结构:链式存储

2. 接口实现

2.1. 定义:结点结构体 和 操作链表的结构体

思路类似 链式队列,需要 头head 和 尾tail 两个指针。

头指针 指向 头结点,尾指针 指向 末个 有效结点。

typedef int DLLNDataType;
typedef struct DoubleLinkListNode
{
    DLLNDataType data;                //数据域
    struct DoubleLinkListNode *next;  //前驱指针域
    struct DoubleLinkListNode *prior; //后驱指针域
} DLLN, *DLL;

头尾指针放到一个操作双向链表的结构体里

typedef struct DoubleLinkList
{
    DLL head; // 头指针
    DLL tail; // 尾指针
    int len;  // 记录链表长度(比下标大1)
} ODLL;

2.2. 创建 空的双向链表

创建 头结点,首尾指针都指向其。

2.3. 插入 结点到 指定位置

					PNew
					  |
        post前驱====新结点====post结点
		   |				    |
	  PTemp->prior		   	  PTemp

2.4. 遍历 打印

可以从头或者尾进行遍历

但注意,结构体存放头尾指针的地址,不要直接操作,需要用伪头尾指针代替操作。

2.5. 删除 指定位置的 结点

思路:

  1. 经过伪指针位置移动, 指向删除结点
  2. 直接链接 被删除结点的 前驱后继 的指针域
  3. 释放被删除结点,长度len--

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

用 遍历 有头单向链表 的思想

用 post 记录下标

2.7. 修改 指定下标位置结点 的数据

思路:

  1. 经过伪指针位置移动, 指向目标结点

2.8. 删除 指定数据的 结点

思路:

  1. 用遍历无头单向链表的方式,依次匹配
  2. 数据相等时,再判断位置
    1. 如果是末尾位置 或 仅有头结点和一个数据节点时候。
      1. 真尾指针前移,伪指针执行删除操作
    1. 处于中间位置时候
      1. 直接将目标结点的 前驱后继 链接起来
      2. 释放被删除结点
    1. 长度len--
  1. 数据不相等,伪指针后移

3. 总结

  1. 指定位置插入、指定位置删除、修改指定位置;这三者移动指针的思路是相同的。
  2. 运用有头,无头单向链表思路。

4. 总体代码

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

// 双向链表 结点 结构体
typedef int DLLNDataType;
typedef struct DoubleLinkListNode
{
    DLLNDataType data;                //数据域
    struct DoubleLinkListNode *next;  //前驱指针域
    struct DoubleLinkListNode *prior; //后驱指针域
} DLLN, *DLL;

// 操作双向链表的结构体
typedef struct DoubleLinkList
{
    DLL head; // 头指针
    DLL tail; // 尾指针
    int len;  // 记录链表长度(比下标大1)
} ODLL;

// 创建 空的双向链表
ODLL *DLLInit(void)
{
    // 1.1 开辟空间 存放 操作双向链表 的结构体
    ODLL *PD = (ODLL *)malloc(sizeof(ODLL));
    if (NULL == PD)
    {
        DEBUG("ODLL malloc err");
        return NULL;
    }

    // 1.2 对 操作双向链表 的结构体 初始化成员
    // 同时开辟 头结点
    PD->head = PD->tail = (DLL)malloc(sizeof(DLLN));
    if (NULL == PD->head) //用头 或 尾 指针都行
    {
        DEBUG("DLL HeadNode malloc err");
        return NULL;
    }
    PD->len = 0; //长度为0

    // 2. 初始化 头结点 成员
    // 头结点 指针域为NULL;用头 或 尾 指针都行
    PD->head->prior = PD->head->next = NULL;

    // 3. 返回 操作双向链表 的结构体 的地址
    return PD;
}

// 遍历 打印
void DLLPrint(ODLL *PD)
{
    // 正向遍历相当于遍历有头单向链表
    printf("正向遍历\n");

    DLL PTemp = PD->head;
    while (PTemp->next != NULL)
    {
        PTemp = PTemp->next;
        printf("%d\t", PTemp->data);
    }
    puts("");

    // 反向遍历相当于遍历无头单向链表
    printf("反向遍历\n");

    DLL Ptemp = PD->tail;
    while (Ptemp != PD->head)
    {
        printf("%d\t", Ptemp->data);
        Ptemp = Ptemp->prior;
    }
    printf("\n");
}

// 插入 结点 到 指定位置
int DLLInsert(ODLL *PD, int post, DLLNDataType data)
{

    // 容错 位置 判断
    // 可以正好等于len,为新位置
    if (post < 0 || post > PD->len)
    {
        DEBUG("err insert post! not at [0,len]!");
        return -1;
    }

    // 创建新结点,保存数据
    DLL Pnew = (DLL)malloc(sizeof(DLLN));
    if (NULL == Pnew)
    {
        DEBUG("new node malloc err");
        return -1;
    }
    // 初始化新结点
    Pnew->data = data;
    Pnew->prior = Pnew->next = NULL;

    // 判断插入位置(三种情况)
    // 1.表尾插,尾结点和新结点链接
    // 长度len总比当前最大下标大1
    if (post == PD->len)
    {
        // **直接在末尾插入,不需要移动**

        Pnew->prior = PD->tail; //pnew指向旧尾本身
        PD->tail->next = Pnew;  //旧尾next指向pnew本身

        // 尾指针后移动
        PD->tail = Pnew;
        // 长度++
        PD->len++;
    }
    // 非末尾 下标len位置插入,即中间插
    else
    {
        // **如下,移动后再插入**

        // 定义伪指针,代替 头 或 尾 指针移动
        DLL Ptemp = NULL;

        // 1. 在中间的前半段插入
        // len为奇数,正好是 [开头,正中间]***
        // len为偶数,正好是 [开头,前半段末个]
        if (post < PD->len / 2)
        {
            // 那就伪指针从头移动
            Ptemp = PD->head;

            // 移动伪头指针,指向post位置的结点
            for (int i = 0; i <= post; i++)
                Ptemp = Ptemp->next;
        }
        else
        {
            // PTemp代替尾指针移动
            Ptemp = PD->tail;

            // 移动伪尾指针
            // 直接从有效下标开始,故i不可等于post
            for (int i = PD->len - 1; i > post; i--)
                Ptemp = Ptemp->prior;
        }

        // 进行插入
        // 思路:先完成新结点的链接,再是其前后。否则断链
        // Ptemp已经指向了post位置结点

        // pnew先和post位置前驱连接,其前驱再链接pnew
        // pnew再先和post链接,post再和pnew链接
        // 最终,pnew成为新post位置的结点,原结点后移
        Pnew->prior = Ptemp->prior;
        Ptemp->prior->next = Pnew;

        Pnew->next = Ptemp;
        Ptemp->prior = Pnew;

        // 插入完成,长度+1
        PD->len++;
    }

    return 0;
}

// 删除 指定位置的 结点
int DLLDeletePost(ODLL *PD, int post)
{
    // 容错判断post位置
    if (post < 0 || post >= PD->len)
    {
        DEBUG("err del, need at [0,len)");
        return -1;
    }

    // 位置判断
    // 尾部删除
    if (post == PD->len - 1)
    {
        PD->tail = PD->tail->prior; //尾指针前移
        free(PD->tail->next);       //释放
        PD->tail->next = NULL;      //置空
    }
    // 中间删除
    else
    {
        DLL PTemp = NULL; //最终指向post位置

        // 中间位置思路,同插入
        if (post < PD->len / 2)
        {
            PTemp = PD->head;

            // 指向post位置结点
            for (int i = 0; i <= post; i++)
                PTemp = PTemp->next;
        }
        else
        {
            PTemp = PD->tail;

            // 这种for写法:效果同插入部分
            // 是 post 和 末下标 相差几个空,就移动几次
            for (int i = 0; i < PD->len - 1 - post; i++)
                PTemp = PTemp->prior;
        }

        // 用post结点的前后指针,链接前驱后继,不用pdel
        PTemp->next->prior = PTemp->prior;
        PTemp->prior->next = PTemp->next;

        free(PTemp);
        PTemp = NULL;
    }

    //长度--
    PD->len--;

    return 0;
}

// 查找 指定数据 首次出现的 下标位置
int DLLFind(ODLL *PD, DLLNDataType data)
{
    // 不可知,最优位置,只能遍历
    DLL Ptemp = PD->head;

    // 记录下标位置
    int post = 0;

    // 遍历有头单向链表
    while (Ptemp->next != NULL)
    {
        Ptemp = Ptemp->next;
        if (Ptemp->data == data)
            return post;
        post++;
    }

    return -1; //找不到
}

// 修改 指定下标位置结点 的数据
int DLLModify(ODLL *PD, int post, DLLNDataType data)
{
    if (post < 0 || post >= PD->len)
    {
        DEBUG("err post to modify, not at [0,len-1]");
        return -1;
    }

    // 去寻找这个位置,让PTemp指向post位置的结点
    DLL PTemp = NULL;

    if (post < PD->len / 2)
    {
        PTemp = PD->head;

        // 有头链表,需要=post
        for (size_t i = 0; i <= post; i++)
            PTemp = PTemp->next;
    }
    else
    {
        PTemp = PD->tail;
        for (size_t i = 0; i < PD->len - 1 - post; i++)
            PTemp = PTemp->prior;
    }

    // 修改数据
    PTemp->data = data;

    return 0;
}

// 删除 指定数据 的所有结点
void DLLDeleteDataNode(ODLL *PD, DLLNDataType data)
{
    // 用遍历 无头单向链表思路即可
    // 暂不能优化

    DLL Ph = PD->head->next; //无头链表的首个结点

    while (Ph != NULL)
    {
        // 先判断 数据是否相等
        if (Ph->data == data)
        {
            // 首先判断,位置是否是 末尾
            // 是的话,优先特殊处理,以符合while判断 和 逻辑处理
            // 应对:只有头结点和一个有效元素情况
            //      和 遍历到最后个 是目标结点
            if (Ph == PD->tail)
            {
                // 真表尾指针 前移
                PD->tail = PD->tail->prior;

                // 新被指向的尾结点,需要next置NULL
                PD->tail->next = NULL;

                // 释放
                free(Ph);
                Ph = NULL;
            }
            // 非末下标位置,是中间位置
            else
            {
                // 这是 删除;区分插入
                // 被删结点 的前后 链接起来 即可
                Ph->prior->next = Ph->next;
                Ph->next->prior = Ph->prior;

                DLL PDel = Ph;
                Ph = Ph->next;

                free(PDel);
                PDel = NULL;
            }

            // 成功删除了,需要长度-1
            PD->len--;
        }
        // 不相等,则 指针 继续移动
        else
            Ph = Ph->next;
    }
}

int main(int argc, char *argv[])
{

    // 创建空的有头双向链表
    // 包括:头结点,操作链表的结构体
    ODLL *PD = DLLInit();

    // 插入结点
    for (int i = 0; i < 6; i++)
        DLLInsert(PD, i, i * 11);

    // 打印链表
    DLLPrint(PD);

    // 删除指定位置数据看看
    puts("----");
    DLLDeletePost(PD, 2);
    DLLPrint(PD);

    // 查看 数据 第一次出现的位置
    printf("%d at post:%d\n", 55, DLLFind(PD, 55));

    // 修改指定 下标位置 的数据
    puts("----");
    DLLModify(PD, 2, 999);
    DLLPrint(PD);
    printf("%d at post:%d\n", 999, DLLFind(PD, 999));

    // 删除所有这个数据 的结点
    puts("----");
    DLLPrint(PD);
    puts("--*************--");

    DLLDeleteDataNode(PD, 55);
    DLLPrint(PD);
}