算法通关村第一关——链表经典问题之ALLIN笔记

当你找不到思路的时候,想想有哪些数据结构

比如说:
常用的数据结构:数组、链表、队、栈、Hash、集合、树、堆。
常用的算法思想:查找、排序、双指针、递归、迭代、分治、贪心、回溯、动态规划。

1.两个链表第一个公共子节点

链表结构

class ListNode(){
	public int val;
	public ListNode next;
	
	ListNode (int x){
	val = x;
	next = null;
	}
}

题目描述:输入两个链表,找出它们的第一个公共子节点。两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。

图:
在这里插入图片描述我的分析:
1.蛮力法,双重循环,两个指针,一个个比对,如果相等就输出该结点的位置或者值。
2.用Map存第一个链表的结点,进行比较,和蛮力法区别在于时间复杂度是o1,哈希能用集合也行。

我的代码:On²,P常数级

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
class Solution {
    ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode p1 = headA;
        ListNode p2 = headB;
        while(p1!=null)
        {
            p2=headB;
            while(p2!=null)
            {
                
                if(p1==p2)
                return p2;
                else {
                    p2=p2.next;
                }
            }
            p1=p1.next;
        }
        return null;
    }
}

栈和集合方法:
栈:虽然两个链表长度不等,但是后面的结点是一致的,所以根据栈先进后出的特点,将两个链表压入两个栈中,先出的都是一致的结点,最晚出的就是第一个公共子节点。

代码:
这里用到了java中的栈和泛型,因为结点类型不是固定的。

class Solution {
    ListNode getIntersectionNode(ListNode headA, ListNode headB) {

        //Creat New Stack
		Stack<ListNode> stackA = new Stack();
		Stack<ListNode> stackB = new Stack();
		
		//push Element
		while(headA!=null){
		stackA.push(headA);
		headA=headA.next;
		}
		
		while(headB!=null){
		stackB.push(headB);
		headB=headB.next;
		}

				//pop Element and compare
  	    //循环结束条件,存在栈为空
  	    ListNode temp = null;
		while(stackA.size()>0 && stackB.size()>0)
        {
            if(stackA.peek()==stackB.peek()){
            temp = stackA.pop();
            stackB.pop();
            }else{
                break; 
            }
        }
        return temp;
}
}

2.判断是否为回文序列

回文序列:一个序列是对称的,有偶数个元素。
我的分析:
1.用数组从中间向两边查找
2.因为是对称的,所以只要翻转过来,每一个对应的值还是相等的就是回文序列。

我的代码:时间复杂度 o (3n) 空间复杂度

class Solution {
    public boolean isPalindrome(ListNode head) {

        ListNode preNode = head;
        Stack <ListNode> stack = new Stack();
// count n
        while(preNode!=null)
        {
            stack.push(preNode);
            preNode = preNode.next;

        }
        /*
        if(stack.size()%2==1)
        {
            return false;
        }
*/
// count 2n
        while(stack.size()>0 && head!=null)
        {
            if(stack.pop().val!=head.val)
            return false;

            head=head.next;
        }
        return true;

    }
}

总结:想不出来,抄。

  • 方法一:将链表元素赋值到数组中,从数组两端向中间对比。但是会被视为逃避链表。

  • 方法二:如上。先压栈,然后遍历比较。

  • 方法三:优化2,压栈得到总长度,遍历一半。省了 三分之一时间

  • 方法四:反转链表,先创建一个链表newList,将原始链表OldList 元素值 逆序保存到newList中,然后重新遍历。。。我的思考:这种方法对于我这样一个新手来说问题在于链表是顺序的,如何将最后一个元素到第一个元素逆序放到新链表中是个问题。但其实很简单,旧链表肯定是不可逆的顺序查找,但是查找过程中,把当前结点作为头结点插到新链表中就可以了。

  • 方法五:优化4,反转一半元素。

  • 方法六:优化5,使用双指针思想中的快慢指针,fast一次走两步,slow一次走一步。当fast到达表尾的时候,slow正好到达一半的位置,那么接下来可以从头开始逆序一半的元素,或者从slow开始逆序一半。

  • 方法七:在遍历的时候使用递归来反转一半的链表。

  • 方法八:如果使用方法六的话,不需要逆序一半的元素,可以直接将fast指向头结点,low指向一半节点的下一个节点然后判断。就不需要逆序的时间复杂度。但是这个方法虽然简单却只能考虑到回文序列是合法的情况。如果回文序列不合法,只有一个两个元素,或者元素不足偶数位就不行。

实例代码:快慢指针+反转,方法六。

class Solution {
    public boolean isPalindrome(ListNode head) {
        ListNode fast = head,low = head;
        while(fast != null && fast.next != null)
        {
            fast = fast.next.next;
            low = low.next;
        }
        fast = head;
        low = reBack(low);
        while(low!=null && low.val == fast.val)
        {
            low = low.next;
            fast = fast.next;
        }
        return low == null ? true:false;
    }

    public ListNode reBack(ListNode head){
        ListNode pre = null,next = null;
        while(head != null){
            next = head.next;
            head.next = pre;
            pre = head;
            head = next;
        }
        return pre;


    }
}

3.合并有序链表

合并有序链表有三种情况,都为空,都不为空,有为空的链表。

3.1 合并两个有序链表

介绍:将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有结点组成的。

我的思路:分为三种情况,链表都为空。部分为空。全不为空。
然后将其中一个链表节点接到另一个链表相应位置上,但是最简单的是直接接到新链表上。

代码:On O2n

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {

        ListNode newhead = new ListNode();
        ListNode newcur = newhead;
        if(list2 == null)
        {
            return list1;
        }

        else if(list1==null )
        {
            return list2;
        }
        else {

            while(list1!=null && list2!=null)
            {
                if(list1.val<list2.val)
                {
                    ListNode temp = list1.next;
                    newcur.next =list1;
                    list1.next = null;
                    newcur = newcur.next;
                    list1 = temp;
                }else{
                    ListNode temp = list2.next;
                    newcur.next =list2;
                    list2.next = null;
                    newcur = newcur.next;
                    list2 = temp;
                }
            }

            if(list1 == null && list2 == null) //if(list1.equals(list2))
            return newhead.next;

            else if (list1==null)
            {
                newcur.next = list2;
                return newhead.next;
            }else{
                newcur.next = list1;
                return newhead.next;
            }
           
        }
    }
}

拓展优化代码:

我的思考:观看上述代码块,不难发现有两个地方有点臃肿,第一,判断链表空的情况可以直接在循环下面写。第二,循环下面的链表连接和第一是重复的可以合并

再看循环体,循环体也很臃肿。我们重新看一遍合并链表的过程。其实是用新链表当前头结点直接去连接小的那个头结点,没必要保存后继而且指空,就直接接到那个表上,然后表头结点下移。

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
         ListNode newhead = new ListNode();
         ListNode newcur = newhead;

            while(list1!=null && list2!=null)
            {
                if(list1.val<list2.val)
                {
                    newcur.next =list1;
                    list1 =list1.next;
                    newcur = newcur.next;
                }else{
                    newcur.next =list2;
                    newcur = newcur.next;
                    list2 = list2.next;
                }
            }
            
            newcur.next = list1 == null ? list2 : list1;
            return newhead.next;
            
            }
           
    
    }

运行完这段代码后,虽然变得简洁了,但是内存用的多了。

3.2 合并 K 个升序链表

代码:时间复杂度偏高

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode cur = null;
        for(ListNode list : lists)
        {
            cur = mergeTwoLists(cur , list);
        }
        return cur;
    }
    ListNode mergeTwoLists(ListNode list1 , ListNode list2)
    {
        ListNode newhead = new ListNode();
         ListNode newcur = newhead;

            while(list1!=null && list2!=null)
            {
                if(list1.val<list2.val)
                {
                    newcur.next =list1;
                    list1 =list1.next;
                    newcur = newcur.next;
                }else{
                    newcur.next =list2;
                    newcur = newcur.next;
                    list2 = list2.next;
                }
            }
            
            newcur.next = list1 == null ? list2 : list1;
            return newhead.next;
            
            }
    }

优化:用归并和堆

3.3 合并两个链表

给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。

请你将 list1 中下标从 a 到 b 的全部节点都删除,并将list2 接在被删除节点的位置。

代码:

class Solution {
    public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
        ListNode lista=list1,listb=list1;
        int i=0,j=0;
        while(lista.next!=null)
        {
            if(i<a-1)
            {
                i++;
                lista=lista.next;
            }
            if(j<b+1)
            {
                j++;
                listb=listb.next;
            }else
            break;
        }

        lista.next = list2;

        while(list2.next!=null)
        list2=list2.next;
        list2.next=listb;
        return list1;
    }
}

4.双指针专题

4.1 寻找中间结点

给你单链表的头结点 head ,请你找出并返回链表的中间结点。

如果有两个中间结点,则返回第二个中间结点。

我的思考:蛮力法:就是能遍历到中间节点并且返回。双指针,这里用快慢指针,快指针停下的时候,如果下一个结点不为空,就返回慢指针下一个结点。如果为空,就返回慢指针。

代码:

class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode fast=head ;
        ListNode low = head;
        while(fast.next!=null && fast.next.next!=null)
        {
            fast=fast.next.next;
            low = low.next;
        }
        if(fast.next==null)
        return low;
        else
        return low.next;
    }
}

4.2 寻找倒数第K个元素

我的思考:快指针走两步,慢指针走一步肯定不行,必须让这两个指针间隔 k ,所以快指针先走,满指针后走。

代码:

class Solution {
    public int kthToLast(ListNode head, int k) {

        ListNode fast=head , slow =head;
       for(int i=1;i<k;i++)
       {
           fast = fast.next;
       }

       while(fast.next!=null)
       {
           fast = fast.next;
           slow = slow.next;
       }
       return slow.val;
    }
}

总结:这里的核心思想就是利用快慢指针的相对位置确定慢指针的位置

4.3 旋转链表

法1 整体

给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。

我的思考:题目就是将尾结点插到头结点插K次,或者是将末尾K个结点接到表头。

结合上一条题目就是将倒数第k+1个结点指向null,然后尾结点指向头结点。
打完代码测试不通过,原因:如果移动节点超过了链表长度,就会出问题。但是可以把这种情况独立出来。

class Solution {
    public ListNode rotateRight(ListNode head, int k) {
        
        if(head==null || head.next==null || k==0)
        {
            return head;
        }

        ListNode temp=head;
        int len = 0;
        while(temp!=null)
        {
            temp=temp.next;
            len++;
        }
        k= k% len;

        if(k==0) return head;
    
        ListNode fast = head,slow = head;

        for(int i=1;i<k;i++)
        {
            fast=fast.next;
        }

        while(fast.next!=null)
        {
            fast=fast.next;
            if(fast.next==null) 
							break;
					
            slow=slow.next;
        }

        ListNode res = slow.next;
        slow.next =null;
        fast.next=head;
        return res;


          
        
    }
}

法2 一个个插

这样的话,把尾结点插到头结点k次是个比较不错的暴力方法。但是时间复杂度一般比较高。不过结点为空或者只有一个节点,或者循环次数过多就会超时。

class Solution {
    public ListNode rotateRight(ListNode head, int k) {

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

        else
        {
            for(int i=0;i<k;i++)
            {
                ListNode end = findEnd(head);
                if(end!=null){
                ListNode temp = end.next;
                end.next = null;
                temp.next = head;
                head = temp;
                }
            }

        return head;
        }
        
    }

public static ListNode findEnd(ListNode head)
    {
        ListNode cur=head;
        while(cur!=null&&cur.next!=null&&cur.next.next!=null)
        {
            cur = cur.next;
        }
        return cur;
    }

    
}

问题:超时了
改进的代码:

class Solution {
    public ListNode rotateRight(ListNode head, int k) {

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

        else
        {
             k = k % getLen(head);

            for(int i=0;i<k;i++)
            {
                ListNode end = findEnd(head);
                if(end!=null){
                ListNode temp = end.next;
                end.next = null;
                temp.next = head;
                head = temp;
                }
            }

        return head;
        }
        
    }

public static ListNode findEnd(ListNode head)
    {
        ListNode cur=head;
        while(cur!=null&&cur.next!=null&&cur.next.next!=null)
        {
            cur = cur.next;
        }
        return cur;
    }

    public static int getLen(ListNode head)
    {
        ListNode temp = head;
        int len=0;
        while(temp!=null)
        {
            temp=temp.next;
            len++;
        }
        return len;
    }

    
}

5.删除链表元素专题

6.再论第一个公共子节点问题

7.链表中是否有环的问题

8.双向循环链表

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

~Yogi

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

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

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

打赏作者

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

抵扣说明:

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

余额充值