线性表基础:链表及经典问题(一)链表的基础知识、典型应用场景、经典算法题刷题(链表的访问)

一、链表的基础知识

1.1 链表的结构

在这里插入图片描述

上图是链表的数据结构,其中head只存储指针,head在C语言中是指针,在Java中相当于一个引用,它本身不是一个对象,也不是一个链表节点

  • 节点

    • 数据域
    • 指针域
      • 实现方式包括:地址(C语言)、下标(相对地址,数组下标)、引用(Java、JS、Python)
  • 链状结构

    • 通过指针域的值形成了一个线性结构

总结:只要在相关的结构中增加了一项指针域,则该结构就可以串成一个链表结构

1.2 访问链表的时间复杂度

链表不适合快速的定位数据,适合动态的插入和删除的应用场景。

  • 查找节点O(n)
  • 插入节点O(1)
  • 删除节点O(1)

1.3 几种经典的链表实现方法

  • 传统方法(节点+指针)

    class ListNode {
        int val;
        ListNode next;
    
        ListNode() {
        }
    
        ListNode(int val) {
            this.val = val;
        }
    }
    
    public class LinkedListImplementation1 {
        public static void main(String[] args) {
        	//构建一个链表
            ListNode head = null;
            head = new ListNode(1);
            head.next = new ListNode(2);
            head.next.next = new ListNode(3);
            head.next.next.next = new ListNode(4);
    
            //定义一个指针pre遍历链表
            ListNode pre = head;
            while (pre != null) {
                System.out.print(String.format("%d->", pre.val));
                pre = pre.next;
            }
            //输出结果1->2->3->4->
        }
    }
    
  • 使用数组模拟

    • 指针域和数据域分离
    • 利用数组存放下标进行索引
    public class LinkedListImplementation2 {
        static int[] data = new int[10];//数据域
        static int[] pointer = new int[10];//指针域
    
        public static void main(String[] args) {
            int head = 4;
            data[head] = 1;
            add(4, 3, 2);
            add(3, 6, 3);
            add(6, 8, 4);
            add(8, 2, 5);
    
            //定义指针pre遍历链表
            int pre = head;
            while (pre > 0) {
                System.out.print(String.format("%d->", data[pre]));
                pre = pointer[pre];
            }
        }
    
        /**
         * @param cur       当前节点指针
         * @param next      下个节点指针
         * @param nextValue 下个节点值
         */
        public static void add(int cur, int next, int nextValue) {
            pointer[next] = pointer[cur];
            pointer[cur] = next;
            data[next] = nextValue;
        }
    
    }
    
  • ……

二、链表的典型应用场景

  • 操作系统内的动态内存分配

    在这里插入图片描述
    在这里插入图片描述
    申请了1GB 后内存产生了两块内存碎片,操作系统是如何维护这两块内存碎片的?
    其中一种实现就是用链表,剩余的内存碎片就会形成一个链表结构
    在这里插入图片描述

  • LRU缓存淘汰算法
    LRU = Least Recently Used(近期最少使用)

    物理设备间数据传输存在速度差异,可以通过将使用较多的数据(热点数据)存放在高速区域,而将使用较少的内容存放在相对低速的区域的方式,来对系统进行优化。通常高速区域广义可以称为缓存。
    在这里插入图片描述

    那么缓存这部分的存储空间内部是怎么维护的呢?最简单的一种维护方式就是链表结构(当然真实情况为了加快查找效率,加了hash表结构,就是hash链表)

    在这里插入图片描述

    如图1G的有限空间内,添加数据向数据4后加,淘汰数据从头部删

  • 还有很多场景:

    • 链表+数组,块状链表,用于大多数语言中可以动态扩容的线性表(例如Java中的HashMap)

三、经典算法题

以下算法题均摘自力扣,读者可自行去力扣答题,并有详细题解,我只是简单介绍了下我的解题思路,并记录一下自己的答案,算法学习个人认为光看、思考是远远不够的,还需要多加练习,以下是链表相关经典算法题答题的推荐顺序:141、142、202、206、92、25、61、24、19、83、82。

3.1 链表的访问

3.1.1 LeetCode #141 环状链表

题目描述
给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。

进阶:你能用 O(1)(即,常量)内存解决此问题吗?

在这里插入图片描述

解题思路

  • 思路1:使用哈希表(额外的存储区)存储已经遍历过的节点
  • 思路2:双指针做法
    使用快慢指针 快指针一次向前2个节点 慢指针一次向前1个节点
    • 有环的链表中 快指针和慢指针最终一定会在环中相遇
    • 无环的链表中 快指针会率先访问到链表尾 从而终结检测过程

解法一:使用哈希表

遍历链表的过程中记录遍历过的节点,如果遇到next节点为null节点,说明没有环,如果遇到我们以前遍历过的节点说明有环。这种做法需要额外的存储区才能完成,而这块存储区域想高效的存储查找节点,就需要hashMap
在这里插入图片描述

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null) {
            return false;
        }
        //hashSet底层就是hashMap
        HashSet<ListNode> hashSet = new HashSet<>();
        do {
            boolean exist = hashSet.add(head);//存在返回false
            if (!exist) {
                return true;
            }
            head = head.next;
        } while (head != null);
        return false;
    }
}

解法二:使用快慢指针

定义两个指针,一开始都指向head节点,然后慢指针每次向前移动一步,快指针每次向前移动两步,进行遍历整个链表:

  • 当快指针走到尾部,即快指针的next节点为null或者快指针本身节点为null时,说明链表没有环
  • 如果链表有环,那么快慢指针一定会相遇,指向同一个节点,当指向同一个节点时,遍历结束

在这里插入图片描述
在这里插入图片描述

什么情况下快指针永远追不上慢指针?

  • 快指针每次走的步数 % 环的总长度 = 慢指针的步数
  • 比如上图,快指针每次走9步,慢指针每次走一步
  • 因为链表形成环最少要3个节点,所以如果慢指针是1步,快指针可以是2步、3步永远也不会有问题
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false;//空或者只有一个节点,肯定没有环
        }
        ListNode slow = head;
        ListNode fast = head;
        do {
            slow = slow.next;
            fast = fast.next.next;
        } while (slow != fast && fast != null && fast.next != null);
        //退出循环两个条件:
        //  快慢节点相遇
        //  快节点走到尾部,因为fast每次走两步,如果链表是单数,刚好走到最后一个节点,如果是双数,会走到null
        return slow == fast;
    }
}

3.1.2 LeetCode #142 环状链表II

题目描述
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。

说明:不允许修改给定的链表。

进阶:你是否可以使用 O(1) 空间解决此题?
在这里插入图片描述

解题思路:

  • 思路1:使用哈希表,和上面题目解题思路完全一样。略
  • 思路2:使用快慢指针,利用一个结论:”相遇点到环起始点的距离”和”链表头节点到环起始点到距离” 永远相等

解法:使用快慢指针

如果有环,观察可以发现:
”相遇点到环起始点的距离”和”链表头节点到环起始点到距离” 永远相等
在这里插入图片描述

证明:
在这里插入图片描述
设链表中环外部分的长度为 a。slow 指针进入环后,又走了 b 的距离与fast 相遇。此时,fast 指针已经走完了环的 n 圈,因此它走过的总距离为 a+n(b+c)+b=a+(n+1)b+nc。

根据题意,任意时刻,fast 指针走过的距离都为 slow 指针的 2 倍。因此,我们有
a+(n+1)b+nc=2(a+b) ⟹ a=nb-b+nc=(n-1)b+nc=c+(n-1)(b+c)

有了 a=c+(n-1)(b+c) 的等量关系,我们会发现:从相遇点到入环点的距离加上 n-1 圈的环长,恰好等于从链表头部到入环点的距离。

而实际上这个n我们实际上是不需要关心的,因此,当发现 slow 与 fast 相遇时,我们再额外使用一个指针 ptr(或者复用快指针)。起始,它指向链表头部;随后,它和 slow 每次向后移动一个位置。最终,它们会在入环点相遇。

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        if(head == null || head.next == null){
            return null;//null或者一个节点肯定没有环
        }
        //先判断是否有环
        ListNode fast = head;
        ListNode slow = head;
        do{
            fast = fast.next.next;
            slow = slow.next;
        }while (fast != slow && fast!= null && fast.next != null);
        
        if(fast == slow){//说明有环
            fast = head;//快指针回到头节点
            while (fast != slow){
                fast = fast.next;
                slow = slow.next;
            }
            return fast;
        }
        return null;
    }
}

3.1.3 LeetCode #202 快乐数

题目描述:
编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果 可以变为 1,那么这个数就是快乐数。

如果 n 是快乐数就返回 true ;不是,则返回 false 。

在这里插入图片描述

解题思路:

核心思想是转化为判断链表是否有环的问题
在这里插入图片描述
在这里插入图片描述

这道题考察的就是逻辑思维结构上的链表思维,即链表唯一指向变换的思维,利用链表唯一指向性的特征,因此本质就是一个链表判环的问题:每一个数字都看成是链表中的节点,每个节点之间转换规则看成是链表的指针,1看成是链表中的尾节点

按照这样的思维,我们怎么证明要么最终遍历到节点1,要么就是无限循环呢?

收敛性的证明

  • 32位int的表示正整数大概是21亿
  • 在这个范围内 各位数字平方和最大的数是1999999999,和为730,意味着构成这个链表中的数字不可能超过730,假设所有数字都出现了,那最多也只有731个节点,1(开始的int数)+ 730
  • 根据鸽巢原理(pigeonhole’s principle,也译作抽屉原理)在730次循环后必定出现重复
class Solution {
    /**
     * 获取下一个数
     *
     * @param value
     * @return
     */
    public int getNext(int value) {
        int result = 0;
        while (value > 0) {
            result += (value % 10) * (value % 10);
            value = value / 10;
        }
        return result;
    }

    public boolean isHappy(int n) {
        if (n < 1) {
            return false;
        }
        int slow = n;
        int fast = n;
        do {
            slow = getNext(slow);
            fast = getNext(getNext(fast));
        } while (slow != fast && fast != 1);
        return fast == 1;
    }
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

犬豪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值