链表基础知识回顾+算法题打卡

链表的概念以及结构

概念:链表是一种物理存储结构上不连续的,非顺序的存储结构,数据元素的顺序是通过节点中的指针来实现的。

结构:(此处指单向非循环链表)链表中每个节点的存储元素一般包含两部分,数据和下一个节点的地址。

struct ListNode {    int val;    ListNode* next;    ListNode(int x) : val(x), next(nullptr) {} };

img

遍历打印链表

在链表中访问元素需要通过每个节点中存放的下一个元素地址,才能找到下一个节点的位置,具体代码实现如下:

//遍历链表 void SListPrint(SListNode* phead) { SList* cur = phead; //这里我们创建一个指针来指向头结点的位置 while(cur) { print("%d->", cur->date); //打印当前节点的数据 cur = cur->next;    //将指针指向下一个节点 } }

申请节点

链表中每一个节点都是动态开辟出来的,所以要新增一个节点之前要先申请一个节点,每个节点的大小为该结构体的大小,开辟成功之后,该节点数据部分存放输入的值,地址(next)置为NULL。

C语言代码展示如下(这部分主要是为了了解具体实现步骤):

//申请节点 SListNode* SListBuyBode(SListDataType x) { SListNode* newnode = (SListNode*)malloc(sizeof(SlistNode)); //申请地址空间 if (newnode == NULL)  //判断是否成功 { perror("nalloc:"); exit(-1); } newnode->data = x; newnode->next = NULL; return newnode; }

用C++则更为简洁且更加安全,代码如下:

struct ListNode {    int val;    ListNode* next;    ListNode(int x) : val(x), next(nullptr) {} }; ​ ListNode* newNode = new ListNode(newValue);

尾插

因为链表无法通过元素下标访问,所以我们要想知道下一个节点的位置,只能通过遍历整个链表。

这里分为两种情况:

  • 链表为空,直接插入
  • 链表不为空,遍历找到尾节点,再插入

//尾插 void SListPushBack(SListNode** head, int x) {    ListNode* newNode = new ListNode(x); //创建一个新节点    if(head == nullptr)   {        *head = newNode; //如果要插入的链表为为空,则直接将新建的节点作为头节点即可   }    else   {        ListNode* current = head;        while(current -> != nullptr)       {            current = current -> next;       }        current->next = newNode;   } }

头插

在头部插入的话,只需要将当前节点置为头节点,再将当前节点的next改为头节点。

//头插 void SListPushFront(SListNode** head, int x) {    //申请新增节点    SListNode* newNode = new SListNode(x);    //将当前节点的next改为头节点    newNode->next = *head;    //将头节点设为当前节点    *head = newNode; }

函数参数 SListNode* head 是值传递,这意味着函数内部对 head 的修改不会影响到函数外部的实参。如果要修改外部的头指针,应该传递头指针的指针(SListNode** head)或者头指针的引用(SListNode*& head

删除

  1. 链表为空,不能删除
  2. 删除头节点:如果要删除的是头节点,先保存头节点,再将下一个节点更新为头节点
  3. 删除其余位置节点

具体代码展示如下:

//删除链表中值为 target的节点 ListNode* deleteNode(ListNode* head, int target) {    //链表为空    if(head == nullptr)   {        return nullptr;   }    //删除头节点    if(head->data == target)   {        //先记下头节点位置        ListNode* temp = head;        head = head -> next;        delete temp;        return head;   }    //删除其余位置    ListNode* cur = head;    //找到目标位置的前一个节点    while(cur->next->data != target && cur->next != nullptr)   {        cur = cur - next;   }    if(cur->next != nullptr)   {        ListNode* temp = cur->next; //记录下要删除节点的下一个节点        cur->next = cur->next->next;//跳过要删除的节点        delete temp;//删除节点   }    return head; }

最后再加一个判断if(cur->next !=nullptr)的原因

目标节点不存在于链表中:如果链表中不存在值为 target 的节点,while 循环会一直执行到链表末尾,此时 current->nextnullptr。当 while 循环条件 current->next != nullptr && current->next->val != target 中的 current->next == nullptr 时,循环终止。在这种情况下,就不应该执行删除操作,因为没有找到要删除的节点。所以 if (current->next != nullptr) 这个判断可以防止在链表中没有目标节点时误删节点或引发空指针异常。

null和nullptr的区别

在 C++ 中,nullptrNULL 都用于表示空指针,但它们之间存在一些重要区别:

  • nullptrnullptr 是 C++ 11 引入的关键字,它的类型是 std::nullptr_t。这是一种特殊的类型,专门用于表示空指针。它可以隐式转换为任何指针类型,使得代码在处理空指针时更加类型安全。例如:

int* ptr1 = nullptr; double* ptr2 = nullptr;

  • NULLNULL 是一个宏,通常在 C 标准库头文件(如 <stdio.h>)或 C++ 兼容头文件中定义。在 C 语言中,它通常被定义为 ((void*)0),在 C++ 中,它通常被定义为 00L(长整型 0)。由于它本质上是整数类型(在 C++ 中),当将 NULL 赋值给指针时,可能会导致一些潜在的类型混淆问题。例如,假设有一个函数重载,同时接受指针和整数类型参数:

void func(int num); void func(int* ptr); ​ func(NULL); // 在C++ 中,这可能会导致编译错误或调用错误的函数,因为NULL可能被解释为整数0 func(nullptr); // 明确调用func(int* ptr)

牛客练习题

BM1 反转链表

/** * struct ListNode { * int val; * struct ListNode *next; * ListNode(int x) : val(x), next(nullptr) {} * }; */ class Solution { public:    /**     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可     *     *     * @param head ListNode类     * @return ListNode类     */    ListNode* ReverseList(ListNode* head) {        // write code here        ListNode* per = nullptr;  //定义前序节点        while(head)  //判断是否到结尾       {            ListNode* temp = head ->next; //记录下一个节点            head ->next = per;  //反转当前节点            per = head; //前序节点向后移            head = temp; //当前节点后移       }        return per;   } };

总结:就是逐个翻转,定义了前序节点和当前节点,逐步翻转,逐步后移,直到链表尾部。

BM2 链表内指定区间反转

/** * struct ListNode { * int val; * struct ListNode *next; * ListNode(int x) : val(x), next(nullptr) {} * }; */ class Solution { public:    /**     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可     *     *     * @param head ListNode类     * @param m int整型     * @param n int整型     * @return ListNode类     */    ListNode* reverseBetween(ListNode* head, int m, int n) {        // write code here        //定义一个表头        ListNode* res = new ListNode(-1);        res -> next = head;        //定义前序节点        ListNode* per = res;        //定义当前节点        ListNode* cur = head;        //找到m位置        for(int i = 1; i < m; i++)       {            per = cur;            cur = cur->next;       }        //从m反转到n        for(int i = m; i < n; i++)       {            ListNode* temp = cur->next;//记录当前节点下一个节点            cur->next= temp->next;//越过当前节点            temp->next = per->next;//当前节点反转            per->next = temp;//前序节点连到最后       }        //返回去掉表头        return res->next;   } };

理解起来是比较抽象,我也是看了半天才懂,反正多写两遍就记住了,下面有张当时理解的手绘图(明天补上)ok已补上

BM3 链表中的节点每k个一组翻转

/** * struct ListNode { * int val; * struct ListNode *next; * ListNode(int x) : val(x), next(nullptr) {} * }; */ class Solution { public:    /**     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可     *     *     * @param head ListNode类     * @param k int整型     * @return ListNode类     */    ListNode* reverseKGroup(ListNode* head, int k) {        // write code here        //找到每次反转的尾部        ListNode* tail = head;        //遍历k次找到当前组的尾部        for(int i = 0; i < k; i++)       {            if(tail == nullptr) return head;            tail = tail->next;       }        //定义所需的前序节点和当前节点        ListNode* per = nullptr;        ListNode* cur = head;        while(cur != tail)       {            ListNode* temp = cur->next;            cur->next = per;            per = cur;            cur = temp;       }        head->next=reverseKGroup(tail, k);        return per;   } };

总结:

  1. 定义一个尾部,遍历k找到当前组的队尾(如果遍历时剩下的不足k个,则直接返回最后一组的head)
  2. 定义前序,当前节点,对当前小组进行翻转
  3. 记下当前组的head(翻转前为头,现在是尾),给head赋值为递归函数的返回值(即下一组的头节点),递归传入当前tail(指向当前组的下一个位置,即下一组的头部)
  4. 返回当前的首部

理解了也不难,总之还是要多写两遍

BM4 合并两个排序的链表

/** * struct ListNode { * int val; * struct ListNode *next; * ListNode(int x) : val(x), next(nullptr) {} * }; */ class Solution { public:    /**     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可     *     *     * @param pHead1 ListNode类     * @param pHead2 ListNode类     * @return ListNode类     */    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {        // write code here        ListNode* res = new ListNode(-1);  //定义一个表头        ListNode* tail = res;  //用来遍历的指针        while(pHead1 != nullptr && pHead2 != nullptr)       {            if(pHead1->val < pHead2->val)           {                tail->next = pHead1;                pHead1 = pHead1->next;           } else {                tail->next = pHead2;                pHead2 = pHead2->next;           }            tail = tail->next;       }        //处理剩余的        if(pHead1 != nullptr)       {            tail->next = pHead1;       }        else {            tail->next = pHead2;       }        return res->next;   } };

这个没什么好说的了,看一下应该都能懂

BM5 合并k个已排序的链表

主要是递归和分治的一个思想,将链表数组不断等分,直到只剩最后两个数组,然后将这两个数组排序合并,在逐步回归合并

主要难点就是递归合并,排序部分跟上一道题一模一样

/** * struct ListNode { * int val; * struct ListNode *next; * ListNode(int x) : val(x), next(nullptr) {} * }; */ class Solution { public:    /**     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可     *     *     * @param lists ListNode类vector     * @return ListNode类     */ ​     //用来合并两个链表    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {        // write code here        ListNode* res = new ListNode(-1);  //定义一个表头        ListNode* tail = res;  //用来遍历的指针        while(pHead1 != nullptr && pHead2 != nullptr)       {            if(pHead1->val < pHead2->val)           {                tail->next = pHead1;                pHead1 = pHead1->next;           } else {                tail->next = pHead2;                pHead2 = pHead2->next;           }            tail = tail->next;       }        if(pHead1 != nullptr)       {            tail->next = pHead1;       }        else {            tail->next = pHead2;       }        return res->next;   }    //划分合并区间    ListNode* divideMerge(vector<ListNode*> &lists, int left, int right)   {        if(left > right) return NULL;        else if(left == right)  //正好剩下最后一个链表       {            return lists[left];       }        //从中间分成两段        int mid = (left + right)/2;        return Merge(divideMerge(lists, left, mid), divideMerge(lists, mid + 1, right));   }    ListNode* mergeKLists(vector<ListNode*>& lists) {        // write code here        //k个列表归并排序        return divideMerge(lists, 0, lists.size() - 1);   } };

能看懂,但是自己完整写下来还是有困难,先这样吧,后面写多了应该会好点。

好的今天就到这里!!!

Read more

数据结构七大排序算法图解——选择排序动图演示

数据结构七大排序算法图解——选择排序动图演示

系列文章目录 四、选择排序 紧接上一篇交换排序 前言: 1、直接选择排序 思想: 例题: 代码部分: 性能分析 2、树形选择排序 思想: 例题一: 例题二: 性能分析 3、堆排序 定义: 方法: 如何“筛选”? 例题: 如何“建初始堆”? 例题: 代码部分 性能分析 4、总结 直接选择排序 树形排序 堆排序 前言: 选择排序的主要思想是每一趟从待排序列中选取一个关键字值最小的记录,也即第 1 趟从 n 个记录中选取关键字值最小的记录,在第 2 趟中,从剩下的 n-1 个记录中选取关键字值最小的记录,直到整个序列中的记录都选完位置。这样,由选取记录的顺序便可得到按关键字值有序的序列。

By Ne0inhk
哈希表完全指南:从入门到刷题实战

哈希表完全指南:从入门到刷题实战

文章目录 * 前置知识要求 * 为什么叫Hash? * 和数组有什么关系? * 数组是怎么组织数据的? * 但如果我知道索引呢? * 矛盾点 * 哈希表的做法 * 对比总结 * 哈希表在代码中长什么样?(Java) * 在 Java 中,哈希表的表现形式为**键值对(Key-Value)** * 键值对是什么? * 底层怎么存的? * 哈希表中常用的方法有哪几个? * 实战:刷LeetCode时怎么用哈希表得到更好的时间复杂度? * 简单题:难度1 * 答案 * 通用小技巧 * 简单题:难度2 * 答案 * 中等题:难度4 * 为什么会有不同的哈希表? * 主要的哈希表种类 * **链表法哈希表(最常见)** * **开放寻址法哈希表** * **布谷鸟哈希(Cuckoo Hashing)** * **一致性哈希(Consistent Hashing)** * 题外话:哈希表的前世今生与永远的更优 * 前世 * 今生 *

By Ne0inhk
Flutter 三方库 lexo_rank_generator 的鸿蒙化适配指南 - 掌控极致资产排序、Jira 级排序算法实战、鸿蒙级精密列表索引专家

Flutter 三方库 lexo_rank_generator 的鸿蒙化适配指南 - 掌控极致资产排序、Jira 级排序算法实战、鸿蒙级精密列表索引专家

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 lexo_rank_generator 的鸿蒙化适配指南 - 掌控极致资产排序、Jira 级排序算法实战、鸿蒙级精密列表索引专家 在鸿蒙跨平台应用执行复杂的数据排序与动态位置认领(如构建一个支持用户拖拽排序的看板系统、处理海量任务的优先级实时重排或是实现一个具备极致写入效能的无限列表索引)时,如果依赖简单的“整数序号(Integer Index)”,极易在处理“中间插入(Re-ranking)”时陷入全量数据更新的性能泥潭导致数据库写入爆炸。如果你追求的是一种完全对齐 Jira 级 LexoRank 排序规范、支持字符串级无限细分且具备极致算法确定性的方案。今天我们要深度解析的 lexo_rank_generator——一个专注于通用 LexoRank 排序算法生成的顶级工具库,正是帮你打造“鸿蒙超感资产调度中心”的核心重器。 前言 lexo_rank_generator 是一套专注于解决“由于频繁插入导致的数据库重排序长尾”

By Ne0inhk
LFU缓存算法全解:从双哈希+双向链表到O(1)艺术,解锁长期热点守护神

LFU缓存算法全解:从双哈希+双向链表到O(1)艺术,解锁长期热点守护神

文章目录 * 本篇摘要 * 一、核心原理 * 二、关键特性与实现机制 * 1. **数据结构设计(高效实现的核心)** * 2. **频率动态更新** * 3.实现思想及代码测试 * 4.为什么LFU用 双哈希表 + 双向链表? * 三、典型优势与劣势 * **优势场景** * **劣势与挑战** * 四、典型问题与优化策略 * 1. **新数据冷启动优化** * 2. **频率衰减(避免历史权重过高)** * 五、适用场景与典型用例 * 六、LFU vs LRU 对比 * 八、一句话总结 * 九、模版源码 * 本篇小结 本篇摘要 一、核心原理 基础规则: 优先淘汰历史访问频率最低的数据(长期统计维度)。 * 每个缓存条目维护两个核心属性:键值对数据 + 访问频率计数器。当缓存容量达到上限时,

By Ne0inhk