Carl代码随想录算法训练营-Day 4-206.反转链表、19.删除链表的倒数第N个节点、160.链表相交、142.环形链表II

每日算法学习记录

206、反转链表

LeetCode题目链接

思路分析

要反转整个链表,只需要将每一个节点的next域指向自己的前置节点就可以了。因此,我们需要两个指针:
current:当前正在操作的节点
previous:当前节点的前置节点
但是同时,我们还要将current的后置节点提前捏在手里,即用一个tmp变量存储后置节点的地址值,否则随着current.next的修改,后面的所有节点都丢了!
在这里插入图片描述

代码展现

由于反转之后,不需要再返回原来的链表头,于是我们可以将给的head变量作为previous指针使用,这样的话在修改完最后的节点时,返回head,也将恰好是优雅地返回新头节点的正确方式。

public ListNode reverseList(ListNode head) {
        ListNode p = head;	//current指针
        head = null;		//previous指针
        while (p != null) {
            ListNode tmp = p.next;	//提前存储后置节点
            p.next = head;			//修改next域为前置节点
            head = p;
            p = tmp;
        }
        return head;
    }

19、删除链表的倒数第N个节点

LeetCode题目链接

思路分析

看到这道题的题目描述中包含的“倒数”、“只扫描一次”等字眼,很多人自然而然地就想到使用递归的算法,因为递归是一次压栈的过程,在触底之后再返回,符合“倒数”。也就是定义一个递归方法remove,功能是返回输入节点的倒数位置。
那么这样的话,每个节点的倒数位置都是后置节点的倒数位置加1。进入下一次递归的操作就是:

int count= 1 + remove(p.next, n);

在返回倒数位置count之前需要判断一下是不是等于n+1,因为要删除倒数第n个点,就需要找到它的前置节点倒数第n+1个节点,进行删除操作。

代码展现

public ListNode removeNthFromEnd(ListNode head, int n) {
            head = new ListNode(-1, head);//虚拟头节点
            remove(head, n);//进入递归方法
            return head.next;//返回真正头节点
        }
        private int remove(ListNode p, int n) {//返回的是当前节点是倒数第几个节点
            if (p.next == null) {//如果p的后置节点为空,说明到尾节点了
                return 1;//尾节点计数为倒数第1个节点
            }
            int count = 1 + remove(p.next, n);//进入递归
            //每个节点开始的链表都是当前节点+后置节点开始的链表
            //所以当前节点的倒数位置=1+后置节点的倒数位置
            if (count == n + 1) {//找到需要删除的节点的前置节点了
                p.next = p.next.next;//删除倒数第n个节点
            }
            return count;//返回当前节点的倒数位置
        }

160、链表相交

LeetCode题目链接

思路分析

需要特别指出的是,本题的链表相交,指的并不是相交处节点存储的数据val相等,而是节点地址相同,也就是A中某节点和B中某节点是同一个节点对象。
本题有一个最符合直觉的思路,那就是如果两个链表相交,那么从某个节点开始都可以看作同一个链表,当遍历A的指针pointerA和遍历B的指针pointerB同时遍历到这个共同子链表的头节点时,就是需要返回的答案节点。
如果两个链表长度相同,则同时从头向后遍历,碰到pointerA==pointerB的情况,就返回;
如果两个链表长度不等,那么可以强行“让它长度相等”,也就时抛弃掉较长的那个链表的前若干个节点,使得开始遍历时两个链表长度相同。另外一种角度来讲,也就是将尾巴对齐。
要做到这个操作,只需要预先计算好两个链表的长度lenAlenB,然后让pointerA先行遍历lenA-lenB个节点即可(假设链表A较长)。
在这里插入图片描述

代码展现

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        int lenA = 0, lenB = 0;
		for (ListNode p = headA; p != null; p = p.next) {
			lenA++;
		}
		for (ListNode p = headB; p != null; p = p.next) {
			lenB++;
		}
		int i = lenA - lenB;
		if (i>0) {
			while (i-- > 0) {
				headA = headA.next;
			}
		} else {
			while (i++ < 0) {
				headB = headB.next;
			}
		}
		while (true) {
			if (headA == headB) {
				return headA;
			}
			headA = headA.next;
			headB = headB.next;
		}
    }

由于要两个链表都要计算长度,因此这将是整个算法中最耗时的环节
复杂度O(Max(lenA, lenB)) = O(n)

142、环形链表II

LeetCode题目链接

思路分析

哈希表法

看到这道题,其实首先就会想到,将遍历过的节点记录在HashSet中,当每遍历到一个节点时,只需要对比一下它是否已存在于HashSet内即可。
但这么做有一个弊端,因为链表理论上可以是无限长,当然不能超过虚拟机的内存大小,但这也远超Java数组的最大长度2^31-1
我们知道HashSet的底层是HashMap实现的,而HashMap的底层就是数组+链表(或红黑树)实现的。那么当我们碰到一条“能绕地球一圈”的链表时,HashMap的占用空间不仅会巨大,而且搜索效率也将变得极低。况且每遍历一个节点就要搜索一次。
这道题或许能用HashSet实现,但一定不是最优解。

双指针法

在处理链表相关的问题时,双指针法是降低时空复杂度的首选。这道题也能使用双指针法,但相对来说会复杂一些,需要一点数学证明过程。
定义一个快指针fast,一个慢指针slow。fast每次移动2个节点,slow每次移动1个节点。这样一来,如果链表没有环,fast很快就会触底;如果链表有环,那么fast和slow最终会相遇,而且是在环内相遇。
在这里插入图片描述

数学证明

如果设:
从头节点到相交点为x个节点
相交点到快慢指针相遇点为y个节点
相遇点经环绕一圈到相遇点为z个节点
在这里插入图片描述

slow移动了x+y个节点,fast移动了x+y+n(y+z)个节点(n>=1)
由于fast每移动2个节点,slow移动1个节点
于是有方程 2 × ( x + y ) = x + y + n × ( y + z ) 2\times (x+y)=x+y+n\times(y+z) 2×(x+y)=x+y+n×(y+z)
化简得到 x = n ( y + z ) − y x=n(y+z)-y x=n(y+z)y
而我们发现,如果从快慢指针的相遇点开始,如果要移动到相交点,则要移动z+m(y+z),其中m>=0,而n>=1,则可以表示为m=n-1,代入可得 z + ( n − 1 ) ( y + z ) = n ( y + z ) − y = x z+(n-1)(y+z)=n(y+z)-y=x z+(n1)(y+z)=n(y+z)y=x
上面的式子说明只要重新设定一个指针i从链表头开始,一个指针j从快慢指针相遇点开始,同时等距移动,那么i和j的相遇点一定是链表环的相交点。
因此,这道题的解法就很明确了

  • 设定fast指针从头节点开始每次移动2个节点,设定slow指针从头节点开始每次移动1个节点,直到相遇。
  • fast指针拨至链表头,和slow指针一起每次移动1个节点。
  • 返回fast指针和slow指针的相遇点。

代码展现

//双指针做法
public ListNode detectCycle(ListNode head) {
            ListNode slow = head, fast = head;
            do {
                if (fast == null || fast.next == null) {
                    return null;
                }
                fast = fast.next.next;
                slow = slow.next;
            } while (slow != fast);
            slow = head;
            while (fast != slow) {
                fast = fast.next;
                slow = slow.next;
            }
            return fast;
        }
//哈希表做法
public ListNode detectCycle(ListNode head) {
            if (head == null) return null;
            Set<ListNode> set = new HashSet<>();
            ListNode _head = new ListNode();
            _head.next = head;
            head = _head;
            while (head.next != null) {
                if (set.contains(head.next)) {
                    return head.next;
                }
                set.add(head.next);
                head = head.next;
            }
            return null;
        }

总结和思考

以上就是今天的算法学习记录,我们通过思路分析、数学证明等手段,完成了四个链表题目:206.反转链表、19.删除链表的倒数第N个节点、160.链表相交、142.环形链表II。其中206题是Day3的内容,但Day3的博客错把今天的题目《两两交换链表内的节点》给写了,就只能调换一下顺序了。
链表的题目说起来就是,要注意头节点和尾节点的特殊情况,这一点我们选择了建立一个虚拟头节点来大幅简化特殊处理头节点的复杂情形。
在修改节点连接顺序的时候,要注意先将会因覆盖而丢失的后置节点临时存储起来,只要缕清思路、勤画图,链表的题目是简单的!

  • 24
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值