LeetCode刷题、(一)数据结构——链表

三月份刷题记录----数据结构——链表

1.P2_两数之和

image-20230328141638832

解题思路:

该问题的重难点在于以下几方面:

1.“满十进一”,有时是下一位加1即可,有时是需要再创建一个新结点(数量级发生变化),针对这点可以设置一个add变量,记录是否需要进1。尤其注意在循环结束之后,若add仍等于1,那么需要创建一个新结点。

2.如果链表长度不同,在对两个链表遍历时,有一个会先变为空值,此时可以将空的部分视为0,执行加和即可。(也相当于看作是相同长度的两个链表)

Code:

public ListNode addTwoNumbers(ListNode l1, ListNode l2){
		ListNode head = new ListNode();
		ListNode l3 =head;
		int add = 0;
		while(l1!=null || l2!=null){//只要有一个不为null就继续
			int x = l1==null?0:l1.val;
			int y = l2==null?0:l2.val;
			int sum = (x+y+add)%10;
			add = (x+y+add)>=10?1:0;
			l3.next = new ListNode(sum);
			l3 = l3.next;
			l1 = l1==null?null:l1.next;
			l2 = l2==null?null:l2.next;
		}
		if (add==1){
			l3.next = new ListNode(1);
		}
	return head.next;
	}

2.P19_删除链表的倒数第 N 个结点

image-20230328141735132

解题思路:

该题目的重点在于定位到倒数第n个节点,实际上根据链表删除元素的方法可以知道,是定位到其前一个节点,也就是倒数第n+1个节点。最粗暴的方法就是先遍历一遍,得到链表的长度N,那么倒数第n+1个,也就是正数第N-n个,只需从头结点移动N-n-1次即可定位到。

但有一种更快的方法,也就是很常用搞的快慢指针法;我目前刷题用到的快慢指针法主要有两种:

1.第一种是先让快指针移动一定的距离,再让快指针和慢指针以同样的步长进行移动,这样可以保证快指针和慢指针始终保持初始的距离。(这种更像是前指针和后指针)

2.第二种是快慢指针同时出发,快指针每次走两步,慢指针每次走一步。

显然这个问题是第一种,也就是在初始状态时先让快指针从头结点出发移动n次,而让慢节点在头结点的前一个(哑节点或伪节点),这样初始时二者中间就相隔了n个节点;当快指针到达null节点时,慢指针恰好在第倒数n+1个位置处。

Code:

    public ListNode removeNthFromEnd(ListNode head, int n) {
		ListNode dummy = new ListNode(0,head);
		ListNode fast=head;
		ListNode slow=dummy;
		// 先让fast领先slow n个身位
		for(int i=0;i<n;i++){
			fast=fast.next;
		}
		//然后双方同时前进,fast到最后一个元素时,slow则在倒数第n个元素前面一个(也就是倒数第n+1个)
		while(fast!=null){
			fast = fast.next;
			slow = slow.next;
		}
		//结束时,slow在要删除的元素位置的前一个位置
		slow.next=slow.next.next;
		return dummy.next;
	}

3.P21_合并两个有序链表

image-20230328141553573

解题思路:

这个题有两种解法,第一种就是常规的迭代:用两个指针指向两个链表的头结点,比较两个值的大小,将一个新结点head的next指向较小的节点,并且较小的一方以及head指针移动,较大的一方指针不动,等待下一次比较,知道有一个指针指到了null。此时若剩下的另一个指针非空,则直接让head的next指向它即可。

第二种解法是使用递归:

递归的定义就是调用自身,设计递归时一般有以下元素和技巧:

1、一定会有退出递归的终止条件,函数不断调用自身,直到在某一层中达到终止条件,拿到返回值开始回溯。

2、问题分解与简化,递归问题都是一个复杂问题逐渐简化与收缩,在一个简单问题上得到返回值后,开始回溯计算出复杂问题的解。

3、设计递归时,可以将其看作一个和自身具有同样功能的其他函数,这样更便于理解。

在这个问题中,leetcode评论中有一个讲解的比较好:

image-20230328160044121

Code:

	public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
		// 递归终止条件
		if (list1==null){
			return list2;
		}
		if(list2 ==null){
			return list1;
		}
//		ListNode l = new ListNode();
		// 递归主体
		if (list1.val<=list2.val){
//			l.next =list1;
			list1.next = mergeTwoLists(list1.next,list2); //找好子问题
			return list1;
		}else{
//			l.next = list2;
			list2.next = mergeTwoLists(list1,list2.next);
			return list2;
		}
	}

4.P23_合并K个升序链表(hard)

image-20230328142024976

解题思路:

这个问题我的解决思路是用到第21题的合并两个有序链表,就是对链表数组循环,借助21题的程序合并相邻的两个有序链表,直到链表数组中所有元素都被合并。

题解中给出的除了我上面的思路之外,还有一个优化后的方法,叫做分治合并,目前还没接触这个方法,后面学习到时再来解决吧。

Code:

public ListNode mergeKLists(ListNode[] lists) {
		if (lists.length==0){
			return null;
		}
		if(lists.length==1){
			return lists[0];
		}
		ListNode l =lists[0];
		for(int i=1;i<lists.length;i++){
			l=merge2Lists(l,lists[i]);
		}
		return l;
    }
	public ListNode merge2Lists(ListNode l1, ListNode l2){
		ListNode p1 =l1;
		ListNode p2 =l2;
		ListNode R = new ListNode();
		ListNode r =R;
		while(p1!=null ||p2!=null){
			if(p1!=null &p2!=null){
				ListNode p =new ListNode(Math.min(p1.val,p2.val));
				r.next = p;
				r=r.next;
				if(p1.val<=p2.val){
					p1=p1.next;
				}else{
					p2=p2.next;
				}
			} else if (p1==null) {
				ListNode p =new ListNode(p2.val);
				r.next = p;
				r = r.next;
				p2 = p2.next;
			}else{
				ListNode p =new ListNode(p1.val);
				r.next = p;
				r = r.next;
				p1 = p1.next;
			}
		}
		return R.next;
	}

5.P24_两两交换链表中的节点

image-20230328142936358

解题思路:

这个题我本身就没有什么思路,看了一点点题解后,给出的是递归方法;主要的思路如下:

1.终止条件:

如果head为空或只有一个元素,则返回head自身。

2.递归过程:

只考虑本层:首先,记录下head.next,将其作为变量next,它将是结果中的头结点;随后,让head.next指向完成交换后的剩余部分(即把head.next.next作为子问题调用函数自身);然后,让next成为头结点,即next.next = head。最后,返回头结点:return next;

Code:

public ListNode swapPairs(ListNode head) {
		if (head == null ||head.next == null) {
			return head;
		}
		ListNode next = head.next;
		head.next = swapPairs(next.next);
		next.next = head;
	return next;
	}

6.P206_反转链表

image-20230328143005211

解题思路1:

可以看出,翻转链表跟第24题很像,因此首先想到的也是递归方法,不过我自己想了很久没有想出来,是看了题解之后才学会的,主要步骤如下:

1.返回条件:head为空或者只有一个元素。

2.递归过程:(这个题的递归过程不太好理解,与一般的递归有点区别)

(这里,正常思路来说,关注本层的话,应该要将后面翻转的结果放在1前面,然后1.next=null; 但是后面部分翻转后,返回的通常是头结点,而不是尾节点,没办法直接将1连接在后面,除非先遍历到最后,再把1接上,但是这样时间复杂度有点高)

这里的做法是,首先让后面的链表完成翻转,即将head.next传入,调用自身(这样最深层处将返回最后一个节点,在倒数第二个节点处执行下面的部分)。此时,可以考虑head=4;先让head.next.next =head(也就是5指向4),然后让head.next =null;(4指向空)。然后将翻转完成的值返回(也就是返回了5)。这样一直进行下去,就可以翻转。

解题思路2:

由于这里递归方法太怪了,看了一下迭代方法,更容易理解一些。思路就是:

设置一个pre变量来记录每一个节点的前一个节点,然后让每一个节点指向前一个节点(记得要更新pre变量)、最终可以得到翻转的链表。

Code:

 public ListNode reverseList(ListNode head) {
        ListNode curr = head;
        ListNode pre = null;
        while(curr!=null){
            // 记录下next
            ListNode next = curr.next;
            // 反向指
            curr.next = pre;
            // 更新pre和curr
            pre = curr;
            curr = next;
        }
        return pre;
    }

7.P92_反转链表 II

image-20230328143029347

解题思路:

这个题我的解法就是对链表进行切割,中间的部分借助上面的翻转链表I函数进行翻转,然后把三个部分连接起来。大概步骤就是:

首先定位到left前一个位置pre1,并用一个指针p记录下来。然后再定位到right前一个位置pre2,把pre2.next赋值给一个新的指针q,然后令pre2.next=null。然后把p.next(也就是4->3->2)传入翻转链表函数,返回一个头节点R。然后令pre1.next = R;然后把R索引到最后一个元素,让R.next =q;最后,返回头结点即可。

显然我的方法很复杂,时间复杂度也比较高,看了一下题解,思路跟我写的是一样的,只不过他没有很蠢地把翻转后的R遍历到最后一个元素,而是还是用之前的p.next。也就是p.next.next=q;另外,它的翻转链表函数并不返回值,只是对链表进行翻转操作。

Code:

public ListNode reverseBetween(ListNode head, int left, int right) {
		if (head==null ||head.next==null){
			return head;
		}
		// 切分链表
		// 先找到left前一个位置
		ListNode dummy =new ListNode(0,head);//只要涉及需要定位前一个位置的,都要加一个哑节点
		ListNode p =dummy;
		for(int i=0;i<left-1;i++){
			p = p.next;
		}
		ListNode First = p;//第一段子链表
		ListNode RightNode = p;
//		first.next = null;
//		ListNode leftNode =p.next;
		for(int j=0;j<right-left+1;j++){
			RightNode = RightNode.next;
		}
		ListNode LeftNode = p.next;
		ListNode Third = RightNode.next;//第三段子链

		RightNode.next =null;//断开,形成第二断子链
		reverseList(LeftNode);//翻转第二段子链
		// 拼接形成新链表
		First.next = RightNode;//此时rightNode为头,leftNode为尾
		LeftNode.next = Third;

		return dummy.next;
	}
public void reverseList(ListNode head){
	ListNode pre = null;
	ListNode curr =head;
	while(curr!=null){
		ListNode next = curr.next;
		curr.next = pre;
		pre = curr;
		curr = next;
	}

8.P25_K个一维数组翻转(hard)

在这里插入图片描述

解题思路:

这道题我用的思路比较杂,也可以说是一点一点试出来的。首先就是用了P206链表翻转函数。(其实这个题也可以使用P92链表翻转Ⅱ函数来完成,就是根据k来每次更新left和right,再把翻转后的子链表连起来。)。这里使用P206的链表翻转函数,在原函数中使用了递归,递归的设计如下:

1.返回条件:剩余节点的个数不超过k(用遍历来判断)

2.递归过程:

首先,把从head前进k个位置作为下一层的输入**(注意:在返回条件那里已经遍历过一次,可以直接把那个指针拿过来用)**来调用自身,意思就是先把后面的给翻转好。

然后,先把当前子链的后面给断开(设为null),借助P206的翻转函数来翻转自身(传入head),然后把翻转之后的尾节点(也就是当前层的head)指向调用自身函数的返回结果(表示指向后面翻转好的部分)。

最后,将借助P206函数翻转自身后的头结点返回即可。

Code:

    public ListNode reverseKGroup(ListNode head, int k) {
		//定义递归返回条件
		ListNode dummy =new ListNode(0,head);
		ListNode p = dummy;
		if (head==null || head.next==null){
			return head;
		}
		int i=0;
		while(p.next!=null){
			i=i+1;
			p=p.next;
			if (i>=k){
				break;
			}
		}
		if (i<k){
			return head;
		}else{
			ListNode R1 = reverseKGroup(p.next,k);
			p.next = null;
			ListNode R2 = reverseList(head);//前一组的后继是反转后的下一组
			head.next=R1;
			return R2;
		}
		// 子问题解决方案


//		return reverse(head);
    }
	public ListNode reverseList(ListNode head){//组内翻转
		if (head==null ||head.next==null){
			return head;
		}
		ListNode next = head.next;
		ListNode R= reverseList(next);
		next.next = head;
		head.next = null;
		return R;
	}

PS: 这道题还有很多借助迭代完成的方法

9.P61_旋转链表

image-20230328143125195

解题思路:

这道题目不难,首先需要定位旋转之后的尾节点,先遍历一遍达到现在的尾节点,并记录下链表长度。然后向右移动k个就相当于把后面的倒数第k个及之后的元素都挪到前面去,也就是说倒数第k个元素将成为头结点;因此在得到链表长度n之后,再次从哑结点出发,前进n-k次,到达倒数第k+1个元素,然后将其后面断开(再次之前先记录下来新的头结点),最后将原来的尾节点和原来的头结点连接起来即可。(利用快慢指针法也可以实现上述过程)

但这个题还有个坑就是k是任取的,也就是说k不一定比链表长度小。这时候只需要用k对链表长度取余即可(因为一个长为n的链表这样移动n次,跟它原来一模一样),得到的余数作为前面提到的要找到的位置。

Code:

public ListNode rotateRight(ListNode head, int k) {
		if (head==null || head.next==null || k==0){
			return head;
		}
		ListNode slow = new ListNode(0,head);
		ListNode fast = head;
		int len = 0;
		for (int i =0;i<k;i++){
			fast = fast.next;
			if (fast==null){ // 说明k大于链表长度,此时的i即为链表长度
				len = i+1;
				break;
			}
		}
		if (len>0){
			if (k%len==0){ // 若整除,直接返回即可
				return head;
			}
			// 若不整除,需要另快指针重新进行移动
			ListNode newfast = head;
			for (int j=0;j<(k%len);j++){
				newfast = newfast.next;
			}
			fast =newfast;
		}
		// 快慢指针以及设定好初始位置(中间相隔k个)
		while(fast!=null){
			fast = fast.next;
			slow = slow.next;
		}
		// 移动完成后,慢指针在需要前移元素的前一个
		ListNode newhead=slow.next;//新的头结点
		ListNode p = newhead;
		//断开原来的连接,否则将变成一个圆圈
		slow.next = null;
		//将原来的尾部和头部连起来
		while(p.next!=null){
			p = p.next;
		}
		p.next = head;
		return newhead;
    }

10.P83_删除链表中的重复元素

在这里插入图片描述

解题思路:

这题也不难,遍历就是了。碰到p.val = p.next.val,就直接p .next = p.next.next(注意这时候千万不要更新p指针的位置,也就是不要去执行p =p.next,因为前面那句话更新了p.next,那p.next.val也变化了,如果是连续多个相同的值,还会接着满足p.val=p.next.val,这时就会再跨过一个,若执行了p = p.next,反而没办法识别这种情况),若不相等,那么要执行p = p.next;

Code:

    public ListNode deleteDuplicates(ListNode head) {
		if (head==null || head.next==null){
			return head;
		}
		ListNode p = head;
		while(p.next!=null){
			if (p.val == p.next.val){
				p.next = p.next.next;
			}else{
				p = p.next;
			}
		}
		return head;
    }

11.P82_删除链表中的重复元素Ⅱ

image-20230328143212871

解题思路:

这道题跟P83的不同之处在于,只要是重复的就完全删除,而不是说还剩下一个。我原本的思路是比较p.next.val和p.next.next.val,这样识别出来时就可以用p来将他们全部删除。但是问题就是,如果识别出来一对,然后剔除掉,那万一后面恰好还有一个跟剔除的值相同的,那就识别不出来了。针对这个问题,我的思路就是如果识别出来了一对的话,那么就从这一对的后面那个开始往后搜索,如果值相同,指针就往后移动,否则就不移动,这样就可以识别出连续多个相同的值,然后再让p直接指到最后停下的位置就可以。

Code:

public ListNode deleteDuplicates(ListNode head) {
		if(head==null || head.next==null){
			return head;
		}
		ListNode R = new ListNode(0,head);
		ListNode p = R;
		while (p.next!=null) {
			int nextval = p.next.next!=null?p.next.next.val:101;
			if (p.next.val==nextval){
				//从此处开始向后遍历,直到出现不相同的值
				ListNode q = p.next.next;
				while(q!=null){
					int nextval2 = q.next!=null?q.next.val:101;
					if (q.val==nextval2){
						q=q.next;
					}else{
						break; //不相同就跳出循环
					}
				}
				p.next = q==null?null:q.next;
			}else{
				p = p.next;
			}
		}
		return R.next;
    }

12.P141_环形链表

image-20230328143231721

解题思路:

P141和P142这两个题目很有意思,二者都是借助之前说的快慢指针法中的第二种情况来做的。也就是fast每次移动两格,slow每次移动一格。在这道题目中,如果存在环结构,那么fast指针一定会和slow指针相遇,即判断fast==slow即可。

Code:

    public boolean hasCycle(ListNode head) {
		if(head==null){
			return false;
		}
      ListNode fast = head;
	  ListNode slow = head;
	  while(fast.next!=null){
		  fast=fast.next.next;
		  slow=slow.next;
		  if(fast==null){
			  return false;
		  }
		  if (fast==slow){
			  return true;
		  }
	  }
	return false;
    }

13.P142_环形链表 II

image-20230328143257575

这个问题是上一个问题的升级版,前面说了如果存在环,那么快指针和慢指针就会相遇,并且相遇时一定满足fast比slow多走了n个环的长度(相遇时一定是在环中相遇,并且相遇时走过的距离差一定是一个环的整数倍,这里有可能前面没有进入环的长度比较长,慢指针还没入环,快指针已经在环中转了好几圈,所以不能说一定是差出一个环的长度,只能说差出了环长度的整数倍),不妨设环的长度为b,而前面不在环中的长度为a。再设fast走过的长度为f,slow走过的长度为s,其中f=2s。那么有:
1. f = 2 s 2. f = s + n b 1.f=2s \\ 2.f=s+nb 1.f=2s2.f=s+nb
故有s=nb,即第一次相遇时慢指针走了环长度的整数倍。

​ 考虑当慢指针走到环入口时,其走了a步。而之后再在环中走,每走一次环的整数倍就会重新回到环的入口。也即慢指针从头结点出发,走nb+a步时都在环的入口处。而第一次相遇时s走了nb步,故只需要再走a步即可。而a是未知数,此时需要再次用到双指针的思想:

​ 重新给定一个慢指针slow2在头结点出发,让slow1和slow2一起移动;当slow2走到环的入口时,其走了a步,而恰巧走a步后slow1也在环入口处,二者重合;此时返回第二次相遇时的结点,即为环的入口;

Code:

    public ListNode detectCycle(ListNode head) {
        if (head==null){
			return head;
		}
		ListNode newhead = new ListNode(0,head);
		//第一次相遇时fast移动了f,slow移动了s,有f=2s
		//设b=len(Cycle),则有f-s = nb;
		// 综上s = nb,即第一次相遇时慢指针走了环长度的整数倍
		// 当慢指针走到环入口时,其走了a=Pos+1步。而之后再在环中走,每走一次环的整数倍就会重新回到环的入口。
		// 也即慢指针从头结点出发,走nb+a步时都在环的入口处。而第一次相遇时s走了nb步,故只需要再走a步即可。
		// 而a是未知数,此时需要再次用到双指针的思想:
		// 重新给定一个慢指针slow2在头结点出发,让slow1和slow2一起移动;
		// 当slow2走到环的入口时,其走了a步,而恰巧走a步后slow1也在环入口处,二者重合;
		// 故返回第二次相遇时的结点,即为环的入口;
		ListNode fast = newhead;
		ListNode slow = newhead;
		ListNode slow2 = newhead;
		// 构造第一次相遇
		while(fast.next!=null) {
			fast = fast.next.next;
			slow = slow.next;
			if (fast == null||fast.next ==null) {
				return null;
			}
			if (fast == slow) {
				break;
			}
		}
		// 构造第二次相遇
		while(true){
			slow2 = slow2.next;
			slow = slow.next;
			if (slow==slow2){
				break;
			}
		}
		return slow;
    }

--------------------------------------未完待续-----------------------------------------

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值