今日学习笔记:双向链表、循环链表、栈

今日学习笔记:双向链表、循环链表、栈

核心目标:掌握双向链表、循环链表的常用操作,理解栈的特性及顺序栈的实现,熟练编写各操作函数,规避常见逻辑错误(如野指针、内存泄漏、循环异常)。

一、双向链表(补充学习)

前置基础:双向链表每个节点包含前驱指针(prev)和后继指针(next),通常采用"带头节点(哨兵节点)"结构,简化空链表、首尾操作的逻辑,头节点不存储有效数据,仅用于占位和统一操作。

节点及链表结构定义:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 双向链表节点结构
typedef struct Node {
    int data;          // 节点存储的数据(int可替换为其他类型)
    struct Node *prev; // 前驱指针,指向前一个节点
    struct Node *next; // 后继指针,指向后一个节点
} Node;

// 双向链表(带头节点)结构
typedef struct {
    Node *head; // 头节点指针(哨兵节点)
} DoubleList;

1. 核心操作及实现要点

(1)头删除(删除第一个有效节点)

操作逻辑:

    1. 判空:头节点的next为NULL(无有效节点),直接返回失败;
    1. 定义临时指针temp,指向待删除节点(phead->next);
    1. 关联后续节点:头节点的next指向temp的next(temp->next);
    1. 若待删除节点不是最后一个节点,需更新其下一个节点的prev,指向头节点;
    1. 释放temp节点内存,避免内存泄漏。

完整代码实现:

c 复制代码
// 头删除函数(参数:双向链表指针,返回值:1成功,0失败)
int double_list_delete_head(DoubleList *list) {
    // 1. 判空:链表为空或无有效节点
    if (list == NULL || list->head->next == NULL) {
        printf("双向链表为空,无法执行头删除!\n");
        return 0;
    }

    // 2. 定义temp指针,指向待删除的第一个有效节点
    Node *temp = list->head->next;

    // 3. 头节点指向待删除节点的下一个节点,跳过待删除节点
    list->head->next = temp->next;

    // 4. 若待删除节点不是最后一个节点,更新其下一个节点的prev
    if (temp->next != NULL) {
        temp->next->prev = list->head;
    }

    // 5. 释放待删除节点内存,避免内存泄漏
    free(temp);
    temp = NULL; // 置空,避免野指针

    printf("双向链表头删除成功!\n");
    return 1;
}

关键注意:不可直接删除phead->next后忽略prev指针的更新,否则会导致链表断裂。

(2)尾删除(删除最后一个有效节点)

操作逻辑:

    1. 判空:phead->next为NULL,返回失败;
    1. 定义指针p遍历链表,找到尾节点(p->next == NULL);
    1. 定义临时指针temp保存尾节点,找到尾节点的前驱节点(p->prev);
    1. 前驱节点的next置为NULL,断开与尾节点的关联;
    1. 释放temp节点内存。

完整代码实现:

c 复制代码
// 尾删除函数(参数:双向链表指针,返回值:1成功,0失败)
int double_list_delete_tail(DoubleList *list) {
    // 1. 判空:链表为空或无有效节点
    if (list == NULL || list->head->next == NULL) {
        printf("双向链表为空,无法执行尾删除!\n");
        return 0;
    }

    // 2. 遍历链表,找到尾节点(p->next == NULL)
    Node *p = list->head->next;
    while (p->next != NULL) {
        p = p->next;
    }

    // 3. 保存尾节点,找到其前驱节点
    Node *temp = p;
    Node *prev_node = p->prev;

    // 4. 前驱节点的next置为NULL,断开与尾节点的关联
    prev_node->next = NULL;

    // 5. 释放尾节点内存
    free(temp);
    temp = NULL;

    printf("双向链表尾删除成功!\n");
    return 1;
}

简化技巧:无需从头遍历,可通过尾节点的prev快速定位前驱,但需确保prev指针指向正确。

(3)删除指定值节点

操作逻辑:

    1. 判空:phead->next为NULL,返回失败;
    1. 定义指针p遍历链表(从phead->next开始),查找data与指定值匹配的节点;
    1. 若未找到匹配节点,返回失败;
    1. 找到节点后,关联其前驱和后继:p->prev->next = p->next;若p不是最后一个节点,p->next->prev = p->prev;
    1. 释放p节点内存。

完整代码实现:

c 复制代码
// 删除指定值节点(参数:链表指针、待删除值,返回值:1成功,0失败)
int double_list_delete_val(DoubleList *list, int val) {
    // 1. 判空:链表为空或无有效节点
    if (list == NULL || list->head->next == NULL) {
        printf("双向链表为空,无法删除指定值节点!\n");
        return 0;
    }

    // 2. 遍历链表,查找与val匹配的节点
    Node *p = list->head->next;
    while (p != NULL && p->data != val) {
        p = p->next;
    }

    // 3. 未找到匹配节点
    if (p == NULL) {
        printf("未找到值为%d的节点,删除失败!\n", val);
        return 0;
    }

    // 4. 关联前驱和后继节点,跳过当前节点p
    p->prev->next = p->next;
    // 若p不是最后一个节点,更新其下一个节点的prev
    if (p->next != NULL) {
        p->next->prev = p->prev;
    }

    // 5. 释放当前节点内存
    free(p);
    p = NULL;

    printf("删除值为%d的节点成功!\n", val);
    return 1;
}

注意:若有多个相同值节点,需明确需求(删除第一个/所有匹配节点),此处默认删除第一个。

(4)销毁链表

操作逻辑(核心:避免野指针和内存泄漏):

    1. 判空:phead为NULL或phead->next为NULL,直接处理头节点后返回;
    1. 定义指针p(指向phead->next)和临时指针temp;
    1. 循环遍历:temp保存当前节点p,p移动到p->next,释放temp,直至p为NULL;
    1. 释放头节点内存,将phead置为NULL(避免后续误操作访问野指针)。

完整代码实现:

c 复制代码
// 销毁双向链表(参数:双向链表指针,释放所有节点内存)
void double_list_destroy(DoubleList *list) {
    // 1. 判空:链表为空,无需销毁
    if (list == NULL || list->head == NULL) {
        printf("双向链表为空,无需销毁!\n");
        return;
    }

    // 2. 定义指针p(指向第一个有效节点)和temp(保存当前节点)
    Node *p = list->head->next;
    Node *temp = NULL;

    // 3. 循环遍历,释放所有有效节点
    while (p != NULL) {
        temp = p;       // 保存当前节点
        p = p->next;    // 移动到下一个节点(释放前先保存下一个节点地址)
        free(temp);     // 释放当前节点
        temp = NULL;    // 置空,避免野指针
    }

    // 4. 释放头节点内存,将链表头指针置空
    free(list->head);
    list->head = NULL;

    printf("双向链表销毁成功!\n");
}

常见错误:仅释放有效节点,忘记释放头节点;或释放节点前未保存下一个节点地址,导致遍历中断。

2. 总结

双向链表的核心是"双向指针联动",所有操作需同时维护prev和next的指向,避免断裂;带头节点结构可统一首尾操作逻辑,减少判空冗余;销毁和删除操作必须释放内存,杜绝内存泄漏。

二、循环链表

核心特性:与双向链表类似,通常带头节点,尾节点的next不指向NULL,而是指向头节点,形成闭环;遍历链表时,从任意节点出发均可遍历所有节点,首尾相连。

节点及链表结构定义:

c 复制代码
#include <stdio.h>
#include <stdlib.h>

// 循环链表节点结构(单向循环,带头节点)
typedef struct Node {
    int data;          // 节点存储的数据
    struct Node *next; // 后继指针,尾节点next指向头节点
} Node;

// 循环链表结构
typedef struct {
    Node *head; // 头节点指针(哨兵节点)
} CircleList;

1. 核心操作及实现要点

(1)创建链表

操作逻辑:

    1. 动态分配头节点内存,判断malloc是否成功(避免内存分配失败导致野指针);
    1. 初始化头节点:prev(双向循环可加)置为NULL,data不赋值(无有效数据);
    1. 闭环初始化:头节点的next指向自身(phead->next = phead),此时为"空循环链表"(无有效节点)。

完整代码实现:

c 复制代码
// 创建空循环链表(返回值:创建成功的链表指针,失败返回NULL)
CircleList *circle_list_create() {
    // 1. 动态分配链表结构内存
    CircleList *list = (CircleList *)malloc(sizeof(CircleList));
    if (list == NULL) {
        printf("链表结构内存分配失败,创建链表失败!\n");
        return NULL;
    }

    // 2. 动态分配头节点内存,判断是否成功
    list->head = (Node *)malloc(sizeof(Node));
    if (list->head == NULL) {
        printf("头节点内存分配失败,创建链表失败!\n");
        free(list); // 释放已分配的链表结构内存
        list = NULL;
        return NULL;
    }

    // 3. 初始化头节点,闭环设置(头节点next指向自身)
    list->head->data = 0; // 头节点无有效数据,可赋值为0占位
    list->head->next = list->head; // 空循环链表,闭环初始化

    printf("循环链表创建成功(空链表)!\n");
    return list;
}

(2)头插(在第一个有效节点前插入新节点)

操作逻辑(核心:维持闭环):

    1. 判空:phead为NULL,返回失败;
    1. 动态分配新节点p_new,赋值data;
    1. 找到尾节点:遍历链表,找到p->next == phead的节点(尾节点);
    1. 关联新节点:p_new->next = phead->next(指向原第一个有效节点);phead->next = p_new;
    1. 维持闭环:尾节点的next指向p_new(确保尾节点始终指向头节点的下一个节点,形成闭环)。

完整代码实现:

c 复制代码
// 循环链表头插(参数:链表指针、插入数据,返回值:1成功,0失败)
int circle_list_insert_head(CircleList *list, int data) {
    // 1. 判空:链表或头节点为空
    if (list == NULL || list->head == NULL) {
        printf("循环链表为空,无法执行头插!\n");
        return 0;
    }

    // 2. 动态分配新节点,赋值data
    Node *p_new = (Node *)malloc(sizeof(Node));
    if (p_new == NULL) {
        printf("新节点内存分配失败,头插失败!\n");
        return 0;
    }
    p_new->data = data;

    // 3. 找到尾节点(p->next == 头节点,即为尾节点)
    Node *p = list->head;
    while (p->next != list->head) {
        p = p->next;
    }

    // 4. 关联新节点,插入到头节点之后(第一个有效节点位置)
    p_new->next = list->head->next;
    list->head->next = p_new;

    // 5. 维持闭环:尾节点next指向新节点
    p->next = p_new;

    printf("数据%d头插成功!\n", data);
    return 1;
}

常见错误:忘记更新尾节点的next,导致闭环断裂;或头插后未更新phead->next,新节点未真正插入。

(3)打印链表

操作逻辑:

    1. 判空:phead->next == phead(无有效节点),提示空链表;
    1. 定义指针p从phead->next开始遍历,循环条件为p != phead(避免遍历死循环);
    1. 打印当前节点data,p移动到p->next,直至回到头节点。

完整代码实现:

c 复制代码
// 打印循环链表(参数:循环链表指针)
void circle_list_print(CircleList *list) {
    // 1. 判空:链表为空或无有效节点
    if (list == NULL || list->head == NULL || list->head->next == list->head) {
        printf("循环链表为空,无数据可打印!\n");
        return;
    }

    // 2. 遍历链表,从第一个有效节点开始,终止条件:p == 头节点
    Node *p = list->head->next;
    printf("循环链表数据(闭环):");
    while (p != list->head) {
        printf("%d ", p->data);
        p = p->next;
    }
    printf("\n");
}

关键:循环终止条件必须是p == phead,否则会陷入无限循环。

(4)查找指定值节点

操作逻辑:

    1. 判空:phead->next == phead,返回NULL(未找到);
    1. 定义指针p从phead->next开始遍历,循环条件p != phead;
    1. 若p->data == 指定值,返回当前节点指针;
    1. 遍历结束未找到,返回NULL。

完整代码实现:

c 复制代码
// 查找指定值节点(参数:链表指针、查找值,返回值:找到返回节点指针,失败返回NULL)
Node *circle_list_find(CircleList *list, int val) {
    // 1. 判空:链表为空或无有效节点
    if (list == NULL || list->head == NULL || list->head->next == list->head) {
        printf("循环链表为空,无法查找节点!\n");
        return NULL;
    }

    // 2. 遍历链表,查找值为val的节点
    Node *p = list->head->next;
    while (p != list->head && p->data != val) {
        p = p->next;
    }

    // 3. 判断是否找到(p == 头节点表示遍历结束,未找到)
    if (p == list->head) {
        printf("未找到值为%d的节点!\n", val);
        return NULL;
    }

    printf("找到值为%d的节点!\n", val);
    return p;
}

(5)头删除(删除第一个有效节点)

操作逻辑:

    1. 判空:phead->next == phead,返回失败;
    1. 定义temp指向待删除节点(phead->next),找到尾节点(p->next == phead);
    1. 头节点的next指向temp->next(跳过待删除节点);
    1. 维持闭环:尾节点的next指向phead->next;
    1. 释放temp节点内存。

完整代码实现:

c 复制代码
// 循环链表头删除(参数:链表指针,返回值:1成功,0失败)
int circle_list_delete_head(CircleList *list) {
    // 1. 判空:链表为空或无有效节点
    if (list == NULL || list->head == NULL || list->head->next == list->head) {
        printf("循环链表为空,无法执行头删除!\n");
        return 0;
    }

    // 2. 定义temp指向待删除节点(第一个有效节点),找到尾节点
    Node *temp = list->head->next;
    Node *p = list->head;
    while (p->next != list->head) {
        p = p->next;
    }

    // 3. 头节点跳过待删除节点,指向其下一个节点
    list->head->next = temp->next;

    // 4. 维持闭环:尾节点next指向新的第一个有效节点
    p->next = list->head->next;

    // 5. 释放待删除节点内存
    free(temp);
    temp = NULL;

    printf("循环链表头删除成功!\n");
    return 1;
}

(6)销毁链表

操作逻辑:

    1. 判空:phead为NULL,直接返回;
    1. 定义指针p = phead->next,temp用于保存当前节点;
    1. 循环遍历:temp = p,p = p->next,释放temp,直至p == phead(遍历完所有有效节点);
    1. 释放头节点内存,将phead置为NULL,彻底销毁闭环。

完整代码实现:

c 复制代码
// 销毁循环链表(参数:循环链表指针,释放所有节点内存)
void circle_list_destroy(CircleList *list) {
    // 1. 判空:链表为空,无需销毁
    if (list == NULL || list->head == NULL) {
        printf("循环链表为空,无需销毁!\n");
        return;
    }

    // 2. 定义指针p(指向第一个有效节点)和temp(保存当前节点)
    Node *p = list->head->next;
    Node *temp = NULL;

    // 3. 循环遍历,释放所有有效节点(终止条件:p == 头节点)
    while (p != list->head) {
        temp = p;       // 保存当前节点
        p = p->next;    // 移动到下一个节点
        free(temp);     // 释放当前节点
        temp = NULL;
    }

    // 4. 释放头节点内存,将链表指针和头节点指针置空
    free(list->head);
    list->head = NULL;
    free(list);
    list = NULL;

    printf("循环链表销毁成功!\n");
}

2. 总结

循环链表的核心是"闭环维护",所有首尾操作(头插、头删、销毁)都需确保尾节点的next始终指向头节点;遍历、查找操作的终止条件是"回到头节点",避免死循环;与双向链表的区别的是,循环链表侧重"闭环遍历",双向链表侧重"双向指针联动"。

三、栈

核心特性:先进后出(LIFO),仅允许在栈顶进行操作(入栈、出栈),栈底元素固定,最先入栈的元素最后出栈;本次学习"顺序栈"(数组实现),结构简单,操作高效。

顺序栈结构定义(所有操作通用):

c 复制代码
#include <stdio.h>
#include <stdlib.h>

#define MAX_SIZE 100  // 顺序栈的最大容量(可根据需求修改)

// 顺序栈结构定义(数组实现)
typedef struct {
    int data[MAX_SIZE];  // 数组存储栈元素(int可替换为其他类型)
    int top;             // 栈顶指针,用数组下标表示,初始为-1(空栈)
} ArrayStack;

1. 核心概念

  • 栈顶:栈中最后入栈、最先出栈的元素,用top指针(数组下标)标记;

  • 栈底:栈中最先入栈、最后出栈的元素,数组实现中固定为下标0;

  • 空栈:top == -1(无任何元素);

  • 栈满:top == MAX_SIZE - 1(MAX_SIZE为栈的最大容量,数组固定);

  • 顺序栈:用数组存储栈元素,top标记栈顶位置,操作均围绕top展开。

2. 核心操作及实现要点

(1)定义栈结构

代码模板:

c 复制代码
#define MAX_SIZE 100  // 栈的最大容量
typedef struct {
    int data[MAX_SIZE];  // 数组存储栈元素(int可替换为其他类型)
    int top;             // 栈顶指针,下标,初始为-1(空栈)
} ArrayStack;  // 顺序栈结构名

(2)创建顺序栈(初始化)

操作逻辑:将栈顶top置为-1,初始化栈为空,无需分配额外内存(数组静态分配)。

关键:初始化必须置top = -1,否则会导致栈空判断失效,出现非法访问。

完整代码实现:

c 复制代码
// 初始化顺序栈(参数:顺序栈指针,将栈置为空栈)
void array_stack_init(ArrayStack *stack) {
    // 判空:栈指针为空,无法初始化
    if (stack == NULL) {
        printf("栈指针为空,无法初始化!\n");
        return;
    }

    // 初始化栈顶top为-1,表示空栈
    stack->top = -1;
    printf("顺序栈初始化成功(空栈)!\n");
}

(3)打印栈

操作逻辑:

    1. 判空:top == -1,提示空栈;
    1. 从栈顶(top)向下遍历至栈底(0),依次打印元素;
    1. 打印顺序:栈顶→栈底(符合栈的查看习惯)。

完整代码实现:

c 复制代码
// 打印顺序栈(参数:顺序栈指针,从栈顶到栈底打印)
void array_stack_print(ArrayStack *stack) {
    // 1. 判空:栈指针为空或空栈
    if (stack == NULL || stack->top == -1) {
        printf("顺序栈为空,无数据可打印!\n");
        return;
    }

    // 2. 从栈顶到栈底打印元素
    printf("顺序栈数据(栈顶→栈底):");
    for (int i = stack->top; i >= 0; i--) {
        printf("%d ", stack->data[i]);
    }
    printf("\n");
}

(4)入栈(push)

操作逻辑(核心:栈顶上移,存入数据):

    1. 判错:栈指针为NULL,或栈满(top == MAX_SIZE - 1),返回失败;
    1. 栈顶上移:top++(先移动下标,再存数据,避免覆盖);
    1. 存入数据:stack->data[top] = 待入栈数据;
    1. 返回成功。

完整代码实现:

c 复制代码
// 顺序栈入栈(参数:栈指针、入栈数据,返回值:1成功,0失败)
int array_stack_push(ArrayStack *stack, int data) {
    // 1. 判错:栈指针为空或栈满
    if (stack == NULL) {
        printf("栈指针为空,无法入栈!\n");
        return 0;
    }
    if (stack->top == MAX_SIZE - 1) {
        printf("栈已满,无法入栈数据%d!\n", data);
        return 0;
    }

    // 2. 栈顶上移,存入数据(先移下标,再存数据)
    stack->top++;
    stack->data[stack->top] = data;

    printf("数据%d入栈成功!\n", data);
    return 1;
}

常见错误:先存数据再top++,会导致top指向的栈顶元素与实际存储位置偏差1。

(5)出栈(pop)

操作逻辑(核心:读取栈顶,栈顶下移,移除元素):

    1. 判错:栈指针为NULL,或空栈(top == -1),返回失败;
    1. 读取栈顶数据:将stack->data[top]赋值给目标变量(使用数据);
    1. 栈顶下移:top--(移除栈顶元素,栈不再管理该元素,无需释放内存,数组静态分配);
    1. 返回成功。

完整代码实现:

c 复制代码
// 顺序栈出栈(参数:栈指针、存储出栈数据的变量指针,返回值:1成功,0失败)
int array_stack_pop(ArrayStack *stack, int *data) {
    // 1. 判错:栈指针为空、数据指针为空或空栈
    if (stack == NULL || data == NULL) {
        printf("参数非法,无法出栈!\n");
        return 0;
    }
    if (stack->top == -1) {
        printf("栈为空,无法出栈!\n");
        return 0;
    }

    // 2. 读取栈顶数据,存入data指向的变量
    *data = stack->data[stack->top];

    // 3. 栈顶下移,移除栈顶元素(无需释放内存,数组静态分配)
    stack->top--;

    printf("数据%d出栈成功!\n", *data);
    return 1;
}

关键理解:出栈不是"仅读取数据",而是"读取+移除",top--后,原栈顶元素不再被栈管理,后续入栈会覆盖该位置;若仅读取不移动top,属于"查看栈顶",不是出栈。

2. 总结

顺序栈的核心是"top指针的操作",所有操作(入栈、出栈、打印)都围绕top展开;入栈判满、出栈判空是必须的,避免数组越界和非法访问;顺序栈操作效率高(时间复杂度O(1)),但容量固定,适合数据量已知、操作简单的场景;核心原则:先进后出,仅栈顶可操作。

四、今日重点回顾与易错点

1. 共性易错点

  • 内存泄漏:链表的删除、销毁操作,未释放节点内存;

  • 野指针:指针未初始化、节点释放后未置空、访问NULL指针的成员;

  • 逻辑断裂:双向链表未同步维护prev和next,循环链表未维持闭环;

  • 死循环:循环链表遍历终止条件错误(未判断p == phead)。

2. 重点记忆

  • 双向链表:双向指针联动,带头节点简化操作,删改必维护prev和next;

  • 循环链表:闭环维护,首尾操作必更新尾节点next,遍历终止于头节点;

  • 顺序栈:先进后出,top=-1为空栈,入栈top++存数据,出栈读数据top--,判满判空是前提。

相关推荐
Larry_Yanan1 天前
QML学习笔记(六十四)动画相关:State状态、Transition过渡和Gradient渐变
开发语言·c++·笔记·qt·学习
ADHD多动联盟1 天前
注意力缺陷是什么?主要有哪些应对策略和干预方法?
学习·学习方法·玩游戏
hsg771 天前
简述:openclaw应用二三事
人工智能·学习
每天回答3个问题1 天前
leetcodeHot100|148.排序链表
数据结构·c++·链表·ue4
承渊政道1 天前
C++学习之旅【unordered_map和unordered_set的使⽤以及哈希表的实现】
c语言·c++·学习·哈希算法·散列表·hash-index
嘉琪0011 天前
Day2 完整学习包(闭包 & 立即执行函数)——2026 0311
学习
南浦别a1 天前
第三十一天--继续学习--TreeSet排序方式和HashSet
学习
承渊政道1 天前
C++学习之旅【⽤哈希表封装myunordered_map和myunordered_set以及位图和布隆过滤器介绍】
数据结构·c++·学习·哈希算法·散列表·hash-index·图搜索算法
金山几座1 天前
C#学习记录-变量与类型
学习·c#
智者知已应修善业1 天前
【花费最少钱加油到最后(样例数据推敲)】2024-11-18
c语言·c++·经验分享·笔记·算法