算法通关村第一关|链表

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

剑指offer52:输入两个链表,找出它们的第一个公共节点。例如下面的两个链表:
在这里插入图片描述
两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。

方法1:通过Hash辅助查找

思路解析:

使用hashmap遍历查找;先将链表1遍历插入到hashmap中,然后遍历链表2,查找hashmap中是否含有key,如果有就返回;

/*
     * 方法1:使用hashmap进行遍历查找
     *  time:O(n) space:O(n)
     */

    public static ListNode findFirstCommonNodeByMap(ListNode pHead1, ListNode pHead2) {
        if (pHead1 == null || pHead2 == null){
            return null;
        }
        ListNode current1 = pHead1;
        ListNode current2 = pHead2;
        HashMap<ListNode,Integer> hashMap = new HashMap<>();
        while (current1 != null){
            hashMap.put(current1,null);
            current1 = current1.next;
        }

        while (current2 != null){
            if (hashMap.containsKey(current2))
                return current2;
            current2  = current2.next;
        }
        return null;
    }

方法2:通过集合来辅助查找

思路解析:

与方法1类似;

 /*
     * 方法2:通过集合辅助查找
     *  time:O(n) space:O(n)
     */
    public static ListNode findFirstCommonNodeBySet(ListNode headA, ListNode headB){
        Set<ListNode> set = new HashSet<>();
        while (headA != null){
            set.add(headA);
            headA = headA.next;
        }

        while (headB != null){
            if (set.contains(headB))
                return headB;
            headB = headB.next;
        }
        return null;
    }

方法3:通过栈

思路解析:

创造两个栈,分别将两个链表插入到两个栈中,然后同时出栈,如果相等就一直出栈,直到不相等时停止出栈,返回最后一次相等时的元素;

/*
     * 方法3:通过栈
     * time:O(n) space:O(n)
     *
     * Stack.peek()
     * peek()函数返回栈顶的元素,但不弹出该栈顶元素。
     * Stack.pop()
     * pop()函数返回栈顶的元素,并且将该栈顶元素出栈。
     *
     */
    public static ListNode findFirstCommonNodeByStack(ListNode headA, ListNode headB){

        Stack<ListNode> stackA = new Stack<>();
        Stack<ListNode> stackB = new Stack<>();

        while (headA != null){
            stackA.push(headA);
            headA = headA.next;
        }

        while (headB != null){
            stackB.push(headB);
            headB = headB.next;
        }

        ListNode preNode = null;
        while (stackA.size() > 0 && stackB.size() > 0){
            if (stackA.peek() == stackB.peek()){
                preNode = stackA.pop();
                stackB.pop();
            }else {
                break;
            }
        }
        return preNode;
    }

方法4:通过序列拼接(等值法)

思路解析:

思路一:

先看下面的链表A和B:
A: 0-1-2-3-4-5
B: a-b-4-5
如果分别拼接成AB和BA会怎么样呢?
AB:0-1-2-3-4-5-a-b-4-5
BA:a-b-4-5-0-1-2-3-4-5
我们发现拼接后从最后的4开始,两个链表是一样的了,自然4就是要找的节点,所以可以通过拼接的方式来寻找交点。这么做的道理是什么呢?我们可以从几何的角度来分析。我们假定A和B有相交的位置,以交点为中心,可以将两个链表分别分为left_a和right_a,left_b和right_b这样四个部分,并且right_a和right_b是一样的,这时候我们拼接AB和BA就是这样的结构:
在这里插入图片描述
我们说right_a和right_b是一样的,那这时候分别遍历AB和BA是不是从某个位置开始恰好就找到了相交的点了?
这里还可以进一步优化,如果建立新的链表太浪费空间了,我们只要在每个链表访问完了之后,调整到一下链表的表头继续遍历就行了,于是代码就出来了;

思路二:

这是「差值法」的另外一种实现形式,原理同样利用「两条链表在相交节点后面的部分完全相同」。

我们令第一条链表相交节点之前的长度为 a,第二条链表相交节点之前的长度为 b,相交节点后的公共长度为 c(注意 c 可能为 0,即不存在相交节点)。

分别对两条链表进行遍历:

当第一条链表遍历完,移动到第二条链表的头部进行遍历;
当第二条链表遍历完,移动到第一条链表的头部进行遍历。
如果存在交点:
第一条链表首次到达「第一个相交节点」的充要条件是第一条链表走了(a + c + b)步,由于两条链表同时出发,并且步长相等,因此当第一条链表走了 (a + c + b) 步时,第二条链表同样也是走了 (a + c + b)步,即 第二条同样停在「第一个相交节点」的位置。

如果不存在交点:两者会在走完 (a + c + b + c)之后同时变为 null, 退出循环。

 /**
     * 方法4:通过序列拼接(等值法)
     * time:O(n) space:O(1)
     */

    public static ListNode findFirstCommonNodeByCombine(ListNode pHead1, ListNode pHead2){
        if (pHead1 == null || pHead2 == null){
            return null;
        }

        ListNode p1 = pHead1;
        ListNode p2 = pHead2;

        while (p1 != p2){
            p1 = p1.next;
            p2 = p2.next;
            if (p1 != p2){
            //一个链表访问完了就跳到另外一个链表继续访问
                if (p1 == null){
                    p1 = pHead2;
                }
                if (p2 == null){
                    p2 = pHead1;
                }
            }
        }
        return p1;
    }

为什么循环体里if(p1!=p2)这个判断有什么作用:
简单来说,如果序列不存在交集的时候陷入死循环,例如 list1是1 2 3,list2是4 5 ,
如果不加判断,list1和list2会不断循环,出不来
list1: 1  2   3   null 5 null  
                   4       4        
list2: 4  5 null   2   3 null    
              1            1     

方法5:通过差值来实现

思路解析:

假如公共子节点一定存在第一轮遍历,假设La长度为L1,Lb长度为L2.则|L2-L1|就是两个的差值。第二轮遍历,长的先走|L2-L1|,然后两个链表同时向前走,结点一样的时候就是公共结点了。

/**
     * 方法5:通过差值来实现
     *  time:O(n) space:O(1)
     * @param pHead1
     * @param pHead2
     * @return
     */

    public static ListNode findFirstCommonNodeBySub(ListNode pHead1, ListNode pHead2){
        if (pHead1 == null || pHead2 == null){
            return null;
        }
        ListNode current1 = pHead1;
        ListNode current2 = pHead2;
        int l1 = 0, l2 = 0;
        //分别统计两个链表的长度
        while (current1 != null){
            current1 = current1.next;
            l1++;
        }

        while (current2 != null){
            current2 = current2.next;
            l2++;
        }
        current1 = pHead1;
        current2 = pHead2;

        int sub = l1 > l2 ? l1 - l2 : l2 - l1;

        //长的先走sub步
        if (l1 > l2){
            int a = 0;
            while (a < sub){
                current1 = current1.next;
                a++;
            }
        }

        //同时遍历两个链表
        while (current2 != current1) {
            current2 = current2.next;
            current1 = current1.next;
        }

        return current1;
    }
  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值