数据结构与算法---链表

题目列表

√ √

  1. https://leetcode-cn.com/problems/reverse-linked-list/
  2. https://leetcode-cn.com/problems/swap-nodes-in-pairs
  3. https://leetcode-cn.com/problems/linked-list-cycle
  4. https://leetcode-cn.com/problems/linked-list-cycle-ii
  5. https://leetcode-cn.com/problems/reverse-nodes-in-k-group/
  6. https://leetcode-cn.com/problems/merge-two-sorted-lists/

经典的链表应用场景,那就是 LRU 缓存淘汰算法

缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的 CPU 缓存、数据库缓存、浏览器缓存等等。缓存的大小有限,当缓存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?
这就需要缓存淘汰策略来决定。常见的策略有三种:先进先出策略 FIFO(First In,First Out)最少使用策略 LFU(Least Frequently Used)最近最少使用策略 LRU(Least Recently Used)

底层的存储结构

在这里插入图片描述

三种最常见的链表结构

单链表、双向链表和循环链表
单链表
在这里插入图片描述
头节点===>记录链表的基地址
尾节点===>指向一个空地址NULL

和数组一样支持查找、插入、删除
但是和数组不同的地方,链表的插入和删除不需要保持内存的连续移动元素,链表本身就是不连续的内存,所以链表的插入和删除一个数据是非常快速的。
时间复杂度是O(1),
但是,在查找效率就差很多了,无法随机访问第k个元素,随机访问时间复杂度为O(n)
在这里插入图片描述
循环链表
和单链表相比,优点是从链尾到链头比较方便,
当处理数据具有环形结构特点时,就特别适合采用循环链表。比如与瑟夫问题。
在这里插入图片描述

双向链表
双向链表优点:可以支持O(1)的时间复杂度情况找到前驱节点,所以茶树和删除操作要比单链表高效
链表删除操作两种情况:
删除结点中“值等于某个给定值”的结点;O(n) 需要遍历查找
删除给定指针指向的结点。单链表O(n) 双链表O(1)

双向链表在有序链表的情况下效率更高
双向链表占用的内存更多,但是应用的却比单向链表更多,主要思想===>空间换时间

没有玩过的全新版本=====>双向循环链表

链表和数组性能比较
在这里插入图片描述
在实际的软件开发中,不能仅仅利用复杂度分析就决定使用哪个数据结构来存储数据。
数组简单易用,在实现上使用的是连续的内存空间,可以借助 CPU 的缓存机制,预读数组中的数据,所以访问效率更高。而链表在内存中并不是连续存储,所以对 CPU 缓存不友好,没办法有效预读。

链表本身没有大小的限制,天然地支持动态扩容
ArrayList 容器,也可以支持动态扩容
如果数组中没有空闲空间了,就会申请一个更大的空间,将数据拷贝过去,而数据拷贝的操作是非常耗时的

如何基于链表实现 LRU 缓存淘汰算法?

我们维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。
当有一个新的数据被访问时,我们从链表头开始顺序遍历链表。

  1. 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据对应的结点,并将其从原来的位置删除,然后再插入到链表的头部。
  2. . 如果此数据没有在缓存链表中,又可以分为两种情况:
    • 如果此时缓存未满,则将此结点直接插入到链表的头部;
    • 如果此时缓存已满,则链表尾结点删除,将新的数据结点插入链表的头部
      不管缓存有没有满,我们都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)

优化 ::散列表(Hash table)来记录每个数据的位置,将缓存访问的时间复杂度降到 O(1)

利用链表解决回文字符串问题

由于回文串最重要的就是对称,那么最重要的问题就是找到那个中心,用快指针每步两格走,当他到达链表末端的时候,慢指针刚好到达中心,慢指针在过来的这趟路上还做了一件事,他把走过的节点反向了,在中心点再开辟一个新的指针用于往回走,而慢指针继续向前,当慢指针扫完整个链表,就可以判断这是回文串,否则就提前退出,总的来说时间复杂度按慢指针遍历一遍来算是O(n),空间复杂度因为只开辟了3个额外的辅助,所以是o(1)

写好链表的小技巧
1. 理解指针和引用的含义

将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量

2. 注意指针丢失和内存泄漏
  • 插入结点时,一定要注意操作的顺序
  • 删除链表结点时,也一定要记得手动释放内存空间
3. 利用哨兵简化实现难度

针对链表的插入、删除操作,需要对插入第一个结点和删除最后一个结点的情况进行特殊处理===>不然这样代码会复杂难懂,。我们也把这种有哨兵结点的链表叫带头链表。相反,没有哨兵结点的链表就叫作不带头链表。使用带头链表就不需要考虑插入时链表为空。。。问题
在这里插入图片描述

4. 注意边界条件处理

代码在一些边界或者异常情况下,最容易产生 Bug
经常用来检查链表代码是否正确的边界条件有这样几个:

  • 如果链表为空时,代码是否能正常工作?
  • 如果链表只包含一个结点时,代码是否能正常工作?
  • 如果链表只包含两个结点时,代码是否能正常工作?
  • 代码逻辑在处理头结点和尾结点的时候,是否能正常工作?
    实际上,不光光是写链表代码,你在写任何代码时,也千万不要只是实现业务正常情况下的功能就好了,一定要多想想,你的代码在运行的时候,可能会遇到哪些边界情况或者异常情况。遇到了应该如何应对,这样写出来的代码才够健壮!
  1. 画图,举例辅助思考 在这里插入图片描述

C++实现链表


//链表类型
typedef struct LNode
{
    ListDataType data;
    struct LNode *next;
}LNode, *LinkList;

//创建n个元素的链表L, 元素值存储在data数组中
Status createList(LinkList &L, ListDataType *data, int n);
//e从链表末尾入链表
Status enList(LinkList &L, ListDataType &e);
//e从链表头取出结点
Status deList(LinkList &L, ListDataType &e);
//遍历
Status travelList(LinkList &L);

//创建n个元素的链表L, 元素值存储在data数组中
Status createList(LinkList &L, ListDataType *data, int n)
{
    LNode *p, *q;
    int i;
    if(n < 0) return OVERFLOW;
    p = L = NULL;

    q = (LNode *)malloc(sizeof(LNode));
    if(NULL == q) return OVERFLOW;
    q->next = NULL;
    p = L = q;

    for(i = 0; i < n; i++)
    {
        q = (LNode *)malloc(sizeof(LNode));
        if(NULL == q) return OVERFLOW;
        q->data = data[i];
        p->next = q;
        q->next = NULL;
        p = q;
    }
    return OK;
}
//e从链表末尾入链表
Status enList(LinkList &L, ListDataType &e)
{
    LNode * p, * q;
    q = (LNode *)malloc(sizeof(LNode));
    if(NULL == q) return OVERFLOW;
    q->data = e;
    q->next = NULL;

    if(NULL == L)
    {   
        L = (LNode *)malloc(sizeof(LNode));
        if(NULL == L) return OVERFLOW;
        L->next = q;
    }
    else if(NULL == L->next)
    {
        L->next = q;
    }
    else
    {
        p = L;
        while(NULL != p->next)
        {
            p = p->next;
        }
        p->next = q;
    }
    
}
//e从链表头取出结点
Status deList(LinkList &L, ListDataType &e)
{
    if(NULL == L || NULL == L->next) return OVERFLOW;
    LNode * p;
    p = L->next;
    e = p->data;
    L->next = p->next;  
    free(p);
    return OK;
}
//遍历
Status travelList(LinkList &L)
{
    if(NULL == L || NULL == L->next) return OVERFLOW;
    for(LNode * p = L->next; NULL != p; p = p->next)
    {
        printf("%d\t", p->data);
    }
    return OK;
}
  1. 多写多练
    . https://leetcode.com/problems/reverse-linked-list/
    https://leetcode.com/problems/swap-nodes-in-pairs
    https://leetcode.com/problems/linked-list-cycle
    https://leetcode.com/problems/linked-list-cycle-ii
    https://leetcode.com/problems/reverse-nodes-in-k-group/

5 个常见的链表操作,把这几个操作都能写熟练,不熟就多写几遍

* 单链表反转

  • 在这里插入图片描述
  • 迭代实现
  • 在这里插入图片描述
  • 递归实现
    在这里插入图片描述

* 链表中环的检测 leetcode 141

  • 快慢指针方法,如果有环,快慢指针必然会相遇。
class Solution {
public:
    bool hasCycle(ListNode *head) {
        ListNode *fast = head, *slow = head;
        while (fast != NULL && fast->next != NULL) {
            fast = fast->next->next;
            slow = slow->next;
            if (fast == slow) return true;
        }
        return false;
        
    }
};

* 两个有序的链表合并

在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode head = ListNode(0);
        ListNode *cur = &head;
        while(l1 != NULL && l2 != NULL)
        {
            if(l1->val >= l2->val)
            {
                cur->next =  l2;
                cur = cur->next;
                l2 = l2->next;
            }
            else
            {
                cur->next = l1;
                cur = cur->next;
                l1 = l1->next;
            }
        }
        if(l1 == NULL) { cur->next = l2;}
        if(l2 == NULL) { cur->next = l1;}

        return head.next;
    }
};

* 删除链表倒数第 n 个结点

在这里插入图片描述

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        int count = 0;
        ListNode *p = head, *q = head;
        // 快指针先走n步
        for(int i = 0; i < n; i++)
        {
            q = q->next;
        }
        if(q == NULL) { return head->next;}  // 删除头节点
        while(q->next != NULL)
        {
            p = p->next;
            q = q->next;
            count++;
        }
        
        p->next = p->next->next;
        return head;
    }
};
  • 求链表的中间结点
/*求链表的中间节点(利用快慢指针)*/
/*链表有奇数个节点时,中间节点只有一个;有偶数个节点时,结果为输出节点和它的下一个*/
struct list_head *find_mid(struct list_head *head)
{
    struct list_head *slow, *fast;
    
    slow = head;/*快慢指针都指向第一个节点*/
    fast = head;
 
    while (fast != NULL && fast->next != NULL && fast->next->next != NULL) {
        slow = slow->next;/*慢指针每次走一步*/
        fast = fast->next->next;/*快指针每次走两步*/
    }
 
    return slow;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值