数据结构与算法(四)——链表(算法题)

上一篇https://blog.csdn.net/MrTreeson/article/details/84890660由于篇幅有限已经解决了两道链表相关的题目,这一篇继续记录一些链表相关算法题的解题方法。注:部分解法来自leetcode讨论区高亮的答案。

环形链表

这题是leetcode上第142题:https://leetcode.com/problems/linked-list-cycle-ii/

题目描述:

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

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

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

方法一:遍历

直接遍历整个数组,一直访问到链表的结尾,如果结点指向null,说明没有环,时间复杂度为O(n)。

但是这种方法比较低效,而且当环存在时,会一直在环内循环,永远得不到输出结果,造成死循环,所以这种方法不可取。

方法二:set集合去重判断

用set集合存储所访问过的结点,set里面是不包含重复元素的,每访问下一个结点,就将它与set中的元素比较,如果已存在则说明有环,并返回这个结点。如果结点指向null了,就说明没有环,返回null。其时间复杂度也为O(n)代码如下:

public ListNode detectCycle(ListNode head) {
        if(head == null || head.next == null)
            return null;
        Set set = new HashSet<>();
        while(head != null) {
            if(head.next == null)
                return null;
            if(set.contains(head)) 
				return head;
            set.add(head);
            head = head.next;
        }
	return null;
}

方法三:快慢指针

定义两个指针,一个快指针fast,一个慢指针slow,快指针每次走两步,慢指针每次走一步,如果没有环,那么快指针会首先为null或者指向null。如果有环,那么在一个环中,两个速度不一致的指针必定会发生相遇,即fast == slow。

判断存在环之后,由于快慢指针相遇前在环中遍历的次数不一定,也不能保证相遇时所在结点就是环的入口,所以还需要额外的判断来找到环的入口:

  • 定义一个新的指针first指向头结点,往后遍历,每次走一步;
  • 慢指针slow也往后遍历,每次走一步;
  • 当first == slow时,二者指向的就是环的入口节点。

代码如下:

    public ListNode detectCycle1(ListNode head) {
		if(head == null || head.next == null)
	            return null;
		
		ListNode fast = head;
		ListNode slow = head;
		while(fast != null && fast.next != null) {
		    fast = fast.next.next;
		    slow = slow.next;
		    if(fast == slow) {
			ListNode first = head;
			while(first != slow) {
			    first = first.next;
			    slow = slow.next;
			    
			}
			return first;
		    }
		}
		return null;
    }

合并两个有序链表

这是leetcode上第21题:https://leetcode.com/problems/merge-two-sorted-lists/
题目描述:

将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

方法一

最简单粗暴的方法就是对每个链表的当前结点进行比较,再定义一个临时结点指向这两个结点比较后的结果,再比较下一个结点,直到链尾。当其中一个结点为空,说明另一个结点所有剩下的结点都比这个结点要大,此时再把这个临时结点指向剩下的结点即可。

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if(l1 == null || l2 == null)
            return l1 == null?l2:l1;
        ListNode result = new ListNode(0);
        ListNode prev = result;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                prev.next = l1;
                l1 = l1.next;
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
            prev = prev.next;
        }
        if (l1 != null) {
            prev.next = l1;
        }
        if (l2 != null) {
            prev.next = l2;
        }
        return result.next;
    }

方法二:递归

    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if(l1 == null || l2 == null)
            return l1 == null?l2:l1;
        if(l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        }else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }

假设除了l1、l2外其它的结点都已经完成合并了,那在比较之后,只需把小的结点指向已经完成合并的链表的头结点即可。对于该头结点而言,也是如此,假设它后面的结点都已经完成了合并,那么只需把它指向已完成的链表的头结点,这就是递归的逻辑。递归的终止条件就是l1或l2为null,返回另一个不为null的值。

回文链表

这是leetcode第234题:https://leetcode.com/problems/palindrome-linked-list/

题目描述:
请判断一个链表是否为回文链表。

示例 1:

输入: 1->2
输出: false
示例 2:

输入: 1->2->2->1
输出: true

方法一:堆栈

不管是判断回文链表还是回文字符串,用栈都是解决这些问题的好方法。把前面一半的链表存入栈中,由于栈是先进后出,所以可以利用弹栈来与head指向的后一半的结点做对比。

需要注意的是,由于链表结点数为奇数和偶数时需要入栈的个数不一样,所以要进行区别处理。
代码如下:

    public boolean isPalindrome(ListNode head) {
		if (head == null || head.next == null) 
		    return true;
		
		ListNode fast = head;
		
		//结点个数为2时直接判断
		if(fast.next.next == null){
	        if(head.val != head.next.val)
	            return false;
	        return true;
		}
		
		Stack stack = new Stack<>();
		
		//利用了快慢指针的思想,当快指针指到最后时head为中点	
		while(fast !=null && fast.next != null) {
		    fast = fast.next.next;
		    stack.push(head.val);
		    head = head.next;
		    
		}
		
		//当结点数为奇数需要把head置为下一个,因为中间结点不需要入栈也不需要判断
		if(fast != null && fast.next == null)
		    head = head.next;
		
		//判断
		while(!stack.isEmpty()) {
		    if(stack.pop() != head.val)
			return false;
		    head = head.next;
		}
		return true;
    }

方法二:快慢指针和反转

与上面类似也使用到快慢指针,但是不使用栈,而是在慢指针走的过程中对所走链表进行反转,当慢指针到达中点后,再从中间开始往左右遍历比较,不相等则返回false。代码如下:

    public boolean isPalindrome(ListNode head) {
		if (head == null || head.next == null) return true;
	        ListNode fast = head;
	        ListNode newHead = null;
	        while (fast != null) {
	            //当结点数为奇数需要把head置为下一个,因为中间结点不需要比较
	            if (fast.next == null) {
	                head = head.next;
	                break;
	            }
	            else {
	                fast = fast.next.next;
	            }
	            
	            //反转链表
	            ListNode next = head.next;
	            head.next = newHead;
	            newHead = head;
	            head = next;
	        }
	        
	        //判断
	        while (newHead != null) {
	            if (newHead.val != head.val) return false;
	            newHead = newHead.next;
	            head = head.next;
	        }
	        
	        return true;
    }

k个一组翻转链表

这是leetcode上第25题:https://leetcode.com/problems/reverse-nodes-in-k-group/

题目描述:

给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。

示例 :

给定这个链表:1->2->3->4->5

当 k = 2 时,应当返回: 2->1->4->3->5

当 k = 3 时,应当返回: 3->2->1->4->5

说明 :

  • 你的算法只能使用常数的额外空间。
  • 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

这道题跟前面反转链表的题目类似,但是比它更难一点,因为需要划分到每k个结点进行反转。

方法一:递归

跟前面一样,也可以用递归来解决这个问题,不同的是需要对链表进行分组,每组进行递归,然后在组内完成链表的反转,再返回该组反转后的头结点。所以,除了递归和反转的步骤之外,还需要有专门进行k分组的模块,代码如下:

    public ListNode reverseKGroup(ListNode head, int k) {
		if(head == null || head.next == null)
	            return head;
		
		ListNode curr = head;
		int count = 0;
		while(curr !=null && count != k) {
		    curr = curr.next;    //得到第k+1个结点
		    count++;
		}
		
		if(count == k) {
		    curr = reverseKGroup(curr, k);
		    while(count > 0) {
				ListNode temp = head.next;
				head.next = curr;
				curr = head;
				head = temp;
				count--;
		    }
		    head = curr;
		}
		
		return head;
    }

这段代码跟前面反转链表的代码其实没什么区别,只是每次递归之后都要重新进行一次k分组,在这个分组里面进行反转的操作。要注意的是,这里的count起始值为0,得到的是curr等于第k+1个结点,因为这样curr才能代表已完成反转的该组链表的头结点,然后用 head.next = curr; 使得当前组的头结点指向该反转后的链表,然后再进行组内后面的反转操作。

使用if(count == k)来判断,当不满足条件也就是剩下的结点数小于k,就可以直接忽略,返回剩下链表的头结点,就可以使这些部分保持原顺序。

方法二:

不使用递归的时候,就需要从头结点开始,首先进行k分组,并在分组内完成反转,这里专门将反转作为一个方法,并返回完成反转后的结尾,该结尾指向下一组要反转的链表。

    public ListNode reverseKGroup(ListNode head, int k) {
	    ListNode begin;
	    if (head==null || head.next ==null || k==1)
	    	return head;
	    ListNode dummyhead = new ListNode(-1);
	    dummyhead.next = head;
	    begin = dummyhead;
	    int i=0;
	    while (head != null){
	    	i++;
	    	if (i%k == 0){ //当head移动到第k个元素时开始反转
	    		begin = reverse(begin, head.next);
	    		head = begin.next;
	    	} else {
	    		head = head.next;
	    	}
	    }
	    return dummyhead.next;
	    
	}

	public ListNode reverse(ListNode begin, ListNode end){
		ListNode curr = begin.next;
		ListNode next, first;
		ListNode prev = begin;
		first = curr;
		while (curr!=end){
			next = curr.next;
			curr.next = prev;
			prev = curr;
			curr = next;
		}
		begin.next = prev;
		first.next = curr;
		return first;
	}


你可能感兴趣的:(数据结构和算法)