剑指offer——有关链表的算法题详解

1、链表的倒数第K个结点

题目描述:输入一个链表,输出该链表中倒数第k个结点。
方法一:倒数,可以利用栈去存储链表的所有结点,然后出栈K个结点,第K个就是倒数第K个结点。

	private static ListNode FindKthToTail(ListNode head, int k) {
		if (head==null || k==0 ) {
			return null;
		}
		Stack<ListNode> stack = new Stack<ListNode>();
		while (head!=null) {
			stack.push(head);
			head = head.next;
		}
		for (int i = 0; i < k - 1; i++) {
			stack.pop();
		}
		if (!stack.isEmpty()) {
			return stack.pop();
		}
		return null;
	}

方法二:采用两个指针,让其中一个指针先走K步,然后两个指针同时向前走,当快指针达到链表尾时,慢指针指向的结点就是倒数第K个结点。如图所示:
在这里插入图片描述
代码如下:

private static ListNode FindKthToTail(ListNode head, int k) {
		if (head == null || k == 0) {
			return null;
		}
		ListNode fast = head ; 
		ListNode slow = head ; 
		for (int i = 0; i < k - 1; i++) {  //倒数K个,应该移动k-1个结点
			fast = fast.next;
			if (fast == null) {
				return null;
			}
		}
		while (fast.next!=null) {
			fast = fast.next;
			slow = slow.next;
		}
		return slow;
	}

2、从尾到头打印链表(递归和非递归)

方法一:利用java集合容器自带的反转方法。
先遍历链表中的每一个结点,使用Collections集合中的反转方法reverse()将链表中的数从尾到头打印。

public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList<Integer> arrayList=new ArrayList<>();
        while (listNode!=null) {
            arrayList.add(listNode.val);
            listNode=listNode.next;
        }
        Collections.reverse(arrayList); 
        return arrayList;
    }

方法二: 根据从尾到头打印,说明后进先出,使用栈去保存链表中的结点,然后再pop出来。代码如下:


public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
		Stack<Integer> stack = new Stack<Integer>();
		while (listNode!=null) {
			stack.push(listNode.val);
			listNode=listNode.next;
		}
		while (!stack.isEmpty()) {
			arrayList.add(stack.pop());
		}
		return arrayList;
    }

方法三:采用递归的方法去保存链表中的数。

 public ArrayList<Integer> printListFromTailToHead(ListNode listNode) {
        ArrayList<Integer> arrayList = new ArrayList<Integer>();
		printLinkList(arrayList,listNode);
		return arrayList;
    }
    private  void printLinkList(ArrayList<Integer> arrayList, ListNode listNode) {
		if (listNode!=null) {
			if (listNode.next !=null) {
				printLinkList(arrayList,listNode.next);
			}
			arrayList.add(listNode.val);
		}
	}

3、链表有关环的算法题

3.1 如何判断一个链表有环

判断一个链表是否有环,我们通常采用两个速度不同的指针,同时移动,如果链表中有环,两个指针最终会在某一个结点相遇。代码如下:

private static boolean isLoop(ListNode head) {
		ListNode fast = head ; 
		ListNode slow = head ; 
		if(head.next==null||head.next.next==null)
            return false;
		fast = head.next.next;
		slow = slow.next;
		while (fast.next!=null&& fast.next.next!=null) {
			if (fast == slow) {
				return true ;
			}
			fast = fast.next.next;
			slow = slow.next;
		}
		return false;
	}

3.2 计算链表中环的大小

解题思路:前面使用快慢指针,当快慢指针有相遇则可以判断链表中有环,这是第一次相遇,当保持一个指针不动,另一个指针移动,那么我们可以从第一次相遇开始计数,当指针第二次相遇时,即可得到环的大小。代码如下:

/**  
	 * @Description: TODO(获取环的大小)
	 * @param head
	 * @return
	 * @author Mr.Wang
	 * @date 2020-07-10 01:10:38 
	 */  
	private static int getLoopSize(ListNode head) {
		
		ListNode current = getLoopNode(head);
		if (current == null) {
			return 0;
		}
		int len = 1;
		ListNode  node = current.next; 
		while (current != node) {
			len++;
			node = node.next;
		}
		return len;
	}
	/**  
	 * @Description: TODO(获取第一次相遇时的结点)
	 * @param head
	 * @return
	 * @author Mr.Wang
	 * @date 2020-07-10 01:10:23 
	 */  
	private static ListNode getLoopNode(ListNode head) {
		ListNode fast = head ; 
		ListNode slow = head ; 
		if(head.next==null||head.next.next==null)
            return null;
		fast = head.next.next;
		slow = slow.next;
		while (fast.next!=null&& fast.next.next!=null) {
			if (fast == slow) {
				return slow ;
			}
			fast = fast.next.next;
			slow = slow.next;
		}
		return null;		
	}

3.3 链表中环的入口结点

题目分析:分两步走:1、判断链表是否有环 2、求环的入口。
在这里插入图片描述
方法一:求出环的大小size,然后让快指针先走size个结点,然后慢指针与快指针同时移动,当快慢指针相遇时,相遇的结点就是入口;代码如下:

private static ListNode EntryNodeOfLoop(ListNode pHead) {
		ListNode fast = pHead;
		ListNode slow = pHead;
		if (isLoop(pHead)) {
			int len = getLoopSize(pHead);
			for (int i = 0; i < len; i++) {
				fast= fast.next;
			}
			while (fast != slow) {
				fast = fast.next;
				slow = slow.next;
			}
			return fast;
		}
		return null;
	}
	/**  
	 * @Description: TODO(获取环的大小)
	 * @param head
	 * @return
	 * @author Mr.Wang
	 * @date 2020-07-10 01:10:38 
	 */  
	private static int getLoopSize(ListNode head) {
		
		ListNode current = getLoopNode(head);
		if (current == null) {
			return 0;
		}
		int len = 1;
		ListNode  node = current.next; 
		while (current != node) {
			len++;
			node = node.next;
		}
		return len;
	}
	/**  
	 * @Description: TODO(获取第一次相遇时的结点)
	 * @param head
	 * @return
	 * @author Mr.Wang
	 * @date 2020-07-10 01:10:23 
	 */  
	private static ListNode getLoopNode(ListNode head) {
		ListNode fast = head ; 
		ListNode slow = head ; 
		if(head.next==null||head.next.next==null)
            return null;
		fast = head.next.next;
		slow = slow.next;
		while (fast.next!=null&& fast.next.next!=null) {
			if (fast == slow) {
				return slow ;
			}
			fast = fast.next.next;
			slow = slow.next;
		}
		return null;		
	}
	/**  
	 * @Description: TODO(判断链表是否有环)
	 * @param head
	 * @return
	 * @author Mr.Wang
	 * @date 2020-07-10 08:12:36 
	 */  
	private static boolean isLoop(ListNode head) {
		ListNode fast = head ; 
		ListNode slow = head ; 
		if(head.next==null||head.next.next==null)
            return false;
		fast = head.next.next;
		slow = slow.next;
		while (fast.next!=null&& fast.next.next!=null) {
			if (fast == slow) {
				return true ;
			}
			fast = fast.next.next;
			slow = slow.next;
		}
		return false;
	}

方法二:先求出快慢指针相遇的结点,然后让快指针回到head处,然后快慢指针以同样的移动速度移动,一定会在环的入口处相遇,相遇的结点就是环的入口。代码如下:

private static ListNode EntryNodeOfLoop(ListNode pHead) {
		if(pHead.next==null||pHead.next.next==null)
            return null;
		ListNode fast = pHead.next.next;
		ListNode slow = pHead.next;
		if (slow ==null || fast == null) {
			return pHead;
		}
		
		while (fast!=null) {
			if (fast == slow) {
				fast = pHead;  //让快指针指向头结点
				while (fast != slow) {
					fast = fast.next;
					slow = slow.next;
				}
				return fast;
			}
			if (fast.next==null||fast.next.next==null || slow.next==null) {
				return null;
			}
			fast = fast.next.next;
			slow = slow.next;
		}
		return null;
	}

此方法代码量很显然比方法一代码量要少,但是有同学可能不是很明白,为什么当快慢结点在环内相遇时,将快结点指向头结点,同时以相同的速度移动,他们会在环的入口处相遇呢???
在这里插入图片描述
A为头结点,B为链表环的入口,M为快慢指针相遇点,则慢指针走的路程x(slow) = l+b,快指针走的路程:x(fast) = l+b+n(a+b),其中n为圈数,且n>=1(如果n=0的快慢指针走的路程一样,不符合逻辑)。有因为快指针一次走两个结点,慢指针一次只走一个结点。所以2x(slow) = x(fast) => l = n(a+b)-b = (n-1)(a+b) + a其中n>=1. a表示M到B点的距离,(a+b)表示环的长度,也就是说,从M点出发,到达B点后,以B为起点绕n-1圈,和以A为起点,走距离为l,两者一定会在B点相遇。

方法三:环的特征可得知,所有结点中只有入口结点会有两个指针同时指向它。使用断链法,在当前结点访问完毕后,断掉指向当前结点的指针。因此,最后一个被访问的结点一定是入口结点。
该方法转载至博客:https://www.cnblogs.com/darlinFly/p/9335380.html
在这里插入图片描述在这里插入图片描述

4、反转链表

算法思路:想要链表反转,只需要将链表里面的指针反转就行。
链表如下:
(1)
先上代码:

 public ListNode ReverseList(ListNode head) {
		if (head==null) {
			return null;
		}
		ListNode pre=null;
		ListNode next=null;
		while (head!=null) {
			next=head.next;   	//(1)
			head.next=pre;	  	//(2)
			pre=head; 			//(3)
			head=next;			//(4)
		}
		return pre;
    }

算法解释如图
代码(1)表示如图:
(2)
使用next指针指向head.next的结点。
代码(2)表示如图:
(3)
红色表示的head.next结点,也就是next指针所指向的结点。将head与next所指向的结点链接断开,将head.next指向pre指向的结点。如图所示;
代码(3)如图:
在这里插入图片描述
将pre指针指向head所指向的指针,使pre指向的结点为反向链表的头结点;
代码(4)如图所示:
在这里插入图片描述

5、两个链表的第一个公共结点

**算法分析:**两个链表的公共结点,只要出现了公共结点,则第一个公共结点后面都是公共的,形成重合链,示意图如下图:
在这里插入图片描述
由上图可知,只需计算两个链表的长度,求出长度差a,让长度长的链表指针先走a步,然后让两个链表指针同时移动,当两个指针指向的结点相同时,该结点就为两个链表的第一个公共结点。代码如下:

private static ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
		ListNode root1 = pHead1;
		ListNode root2 = pHead2;
		int length1=0,length2=0;
		while (root1!=null) {
			length1++;
			root1 = root1.next;
		}
		while (root2!=null) {
			length2++;
			root2 = root2.next;
		}
		int len = 0;
		if (length1<=length2) {
			len = length2-length1;
			for (int i = 0; i < len; i++) {
				pHead2 = pHead2.next;
			}
		}else
		{
			len = length1 - length2;
			for (int i = 0; i < len; i++) {
				pHead1 = pHead1.next;
			}
		}
		while (pHead1 != pHead2) {
			pHead1 = pHead1.next;
			pHead2 = pHead2.next;
		}
		return pHead1;
	}

6、合并两个排序的链表

合并两个排序的链表有两种思路:
(1)非递归方法
代码如下:

   public ListNode Merge_2(ListNode list1,ListNode list2) {
    	if(list2 == null) {
    		return list1;
    	}
    	else if(list1 == null) {
    		return list2;
    	}
    	ListNode mergeHead = new ListNode(0);
    	ListNode current = mergeHead;
    	while(list1 != null && list2 != null) {
    		if(list1.val < list2.val) {
    			current.next = list1;
    			list1 = list1.next;
    		}
    		else {
    			current.next = list2;
    			list2 = list2.next;
    		}
    		current = current.next;
    	}
    	if(list1 == null) {
    		current.next = 	list2;
    	}
    	if(list2 == null) {
    		current.next = list1;
    	}
    	return mergeHead.next;
    }

(2)递归方法
合并两个排序链表过程中,每一次合并一个结点的操作都是重复的,并且当某一链表的结点为null时,递归结束。大概步骤如下:
1)分别取两个链表头结点,head1、head2
2)比较head1与head2大小
3)如果head1小,将head1保存在合并之后的链表中,将head1.next 作为新的头结点;反之,将head2保存在合并之后的链表中,将head2.next 作为新的头结点;
4)重复1)、2)、3)。
代码如下:

public ListNode Merge(ListNode list1,ListNode list2) {
        	if(list1 == null){
	            return list2;
	        }
	        if(list2 == null){
	            return list1;
	        }
	        if(list1.val <= list2.val){
	            list1.next = Merge(list1.next, list2);
	            return list1;
	        }else{
	            list2.next = Merge(list1, list2.next);
	            return list2;
	        }   
    }

7、单链表在时间复杂度为O(1)删除链表结点

算法分析:初次看到题目,有两种思路,分别是(1)找到该结点的上一个结点,让上一个结点的next指向删除结点的下一个结点即可。如图:
在这里插入图片描述
要想找到删除结点的上一个结点必须遍历链表寻找,复杂度为o(n),不满足题意
(2)用该结点的下一个结点,覆盖要被删除的结点。代码如下:

  public void deleteNode(ListNode node) {
        node.val=node.next.val;   //val值 覆盖
        node.next=node.next.next;  //将node指针指向node下下个结点
    }

在这里插入图片描述

总结

这几种算法的总结希望可以帮助到大家对链表这种数据结构的理解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值