《剑指offer升级打怪》--链表

本文精选《剑指Offer》中的链表题目,通过实例解析输入链表输出倒数第k个节点、从尾到头打印链表节点值、寻找链表环入口等常见面试题的实现方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

                                                                             《剑指offer升级打怪》--链表(上)

写在前面:

    本人入门,以下纯属学习记录,若有错误请指正,还有代码有些是参考的。今天凌晨了(估计是疯了,MD明天怎么上班再见),来刷一篇,攒了好久了,刷剑指offer,刷的蛋疼。下面总结下其中关于链表的题目。

题目有:

画个图显得题目好多安静


这里说明一下,我个人是个彩笔,有些代码是从网上的参考(你要说抄袭,这个我承认。)

先来看个简单的:

输入一个链表,输出该链表中倒数第k个结点。
代码如下:

public class FindKthToTailSol {
    public ListNode FindKthToTail(ListNode head, int k) {

        Map<Integer, ListNode> map = new HashMap<>();
        int index = 0;
        while (head != null) {
            map.put(index, head);
            index++ ;
            head = head.next;
        }
        if (k > index || k <= 0) {
            return null;
        }
        return map.get(index - k);
    }

    @Test
    public void test() {
        ListNode node6 = new ListNode(5, null);
        ListNode node5 = new ListNode(4, node6);
        ListNode node4 = new ListNode(4, node5);
        ListNode node3 = new ListNode(3, node4);
        ListNode node2 = new ListNode(3, node3);
        ListNode node1 = new ListNode(2, node2);
        ListNode head = new ListNode(1, node1);
        System.out.println("看下结果:" + FindKthToTail(head, 8).val);
    }
}
再来看个类似的:

输入一个链表,从尾到头打印链表每个节点的值。
代码如下:

public class PrintListFromEndToFront {

    public static ArrayList<Integer> printListFromTailToHead(ListNode listNode) {

        ArrayList<Integer> list = new ArrayList<Integer>();
        Stack<Integer> stack = new Stack<>();
        while (listNode != null) {
            stack.push(listNode.val);
            listNode = listNode.next;
        }
        while (!stack.isEmpty()) {
            list.add(stack.pop());
        }
        for (Integer integer : list) {
            System.out.println(integer);
        }

        return list;

    }

    @Test
    public void test() {
        // {1,2,3,3,4,4,5}
        // ListNode node7 = new ListNode(7, null);
        ListNode node6 = new ListNode(5, null);
        ListNode node5 = new ListNode(4, node6);
        ListNode node4 = new ListNode(4, node5);
        ListNode node3 = new ListNode(3, node4);
        ListNode node2 = new ListNode(3, node3);
        ListNode node1 = new ListNode(2, node2);
        ListNode head = new ListNode(1, node1);
        System.out.println("sss  " + printListFromTailToHead(head));
    }
}
为什么类似,因为在我的解法里,我都借助了其他的容器来完成的,前者用的是map(用list是一样的,我第一次写就是用list做的),后者用的是个stack。

有了前面这两道题做铺垫,来看个装逼的题目,我第一次看到就想,这TM什么题目。不过我自己做出来了大笑,我都做出来了,看官就不必谦虚了。

题目:

一个链表中包含环,请找出该链表的环的入口结点。
代码:

public class EntryNodeOfLoop {
    public ListNode entryNodeOfLoop(ListNode pHead) {
        // 错误原因是:当出现重复元素时,不能作为环
        // List<Integer> list = new ArrayList<>();
        List<ListNode> list = new ArrayList<>();
        //
        while (pHead != null) {
            if (list.contains(pHead)) {
                return pHead;
            }
            else {
                list.add(pHead);
                pHead = pHead.next;
            }
        }
        return null;

    }

    @Test
    public void test() {
        ListNode node6 = new ListNode(5, null);
        ListNode node5 = new ListNode(4, node6);
        ListNode node4 = new ListNode(4, node5);
        ListNode node3 = new ListNode(3, node4);
        ListNode node2 = new ListNode(3, node3);
        ListNode node1 = new ListNode(2, node2);
        ListNode head = new ListNode(1, node1);
        entryNodeOfLoop(head);
    }
}
有了前面两到题,这道题代码看起来确实很简单,关键点在于java的List.contain()方法应用。
看到我的注释,看官就应该知道下个题目了:

在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
代码:

public class Solution {
    public ListNode deleteDuplication(ListNode pHead)
    {
         // 创建一个头结点
        ListNode firstNode = new ListNode(-1);
        // 将pHead结点赋值给头结点的下一个结点first.next
        firstNode.next = pHead;
        // 创建一个last结点,此处考察的关键点是对last进行修改,first结点是否也跟着修改
        ListNode lastNode = firstNode;
        // 外层循环控制条件,当pHead!=null&&pHead.next!=null
        while (pHead != null && pHead.next != null) {
            // 判断当前结点Val与其下个结点的Val是否相等
            if (pHead.val == pHead.next.val) {
                int dupNodeVal = pHead.val;
                while (pHead != null && pHead.val == dupNodeVal) {
                    pHead = pHead.next;
                }
                lastNode.next = pHead;
            }
            else {
                lastNode = pHead;
                pHead = pHead.next;
            }
        }
        return firstNode.next;

    }
}
这道题我没做出来,我没做出来,我没做出来。关键点:在于内层的while循环的使用,经典。为什么后面会总结。

再来看个装逼的题目:

输入两个链表,找出它们的第一个公共结点。
代码:

public class FindFirstCommonNode {
    public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {

        List<ListNode> list = new ArrayList<>();
        while (pHead1 != null) {
            list.add(pHead1);
            pHead1 = pHead1.next;
        }
        while (pHead2 != null) {
            if (list.contains(pHead2)) {
                System.out.println(pHead2.val);
                return pHead2;
            }
            pHead2 = pHead2.next;
        }


        return null;
    }

    @Test
    public void test() {
        ListNode node7 = new ListNode(7, null);
        ListNode node6 = new ListNode(3, node7);
        ListNode node5 = new ListNode(2, node6);
        ListNode node4 = new ListNode(-2, node5);

        ListNode node1 = new ListNode(7, node5);
        ListNode head = new ListNode(1, node1);

        findFirstCommonNode(head, node4);
    }
}


惊不惊喜,意不意外。

再看个题目:

输入两个单调递增的链表,输出两个链表合成后的链表,当然我们需要合成后的链表满足单调不减规则。

代码:

public class MergeList {
    public ListNode Merge(ListNode list1, ListNode list2) {

        ListNode firstNode = null;
        ListNode tmp = null;

        if (list1 == null) {
            return list2;
        }
        if (list2 == null) {
            return list1;
        }
        while (list1 != null && list2 != null) {
            if (list1.val < list2.val) {
                if (firstNode == null) {
                    firstNode = tmp = list1;
                }
                else {
                    tmp.next = list1;
                    tmp = tmp.next;
                }
                list1 = list1.next;
            }
            else {
                if (firstNode == null) {
                    firstNode = tmp = list2;
                }
                else {
                    tmp.next = list2;
                    tmp = tmp.next;
                }
                list2 = list2.next;
            }

        }
        if (list1 == null) {
            tmp.next = list2;
        }
        else {
            tmp.next = list1;
        }

        return firstNode;

    }

    @Test
    public void test() {
        ListNode node6 = new ListNode(56, null);
        ListNode node5 = new ListNode(23, node6);
        ListNode node4 = new ListNode(18, node5);
        ListNode node3 = new ListNode(9, node4);
        ListNode node2 = new ListNode(8, node3);
        ListNode node1 = new ListNode(5, node2);
        ListNode head = new ListNode(1, node1);

        ListNode node7 = new ListNode(19, null);
        ListNode node8 = new ListNode(17, node7);
        ListNode node9 = new ListNode(16, node8);
        ListNode node10 = new ListNode(14, node9);
        ListNode node11 = new ListNode(12, node10);
        ListNode node12 = new ListNode(10, node11);
        ListNode head1 = new ListNode(0, node12);

        ListNode firstNode = Merge(head, head1);
        while (firstNode != null) {

            System.out.println(firstNode.val);
            firstNode = firstNode.next;
        }
    }
}
这个是数据结构书的原题,放到这儿主要是和重复结点的那道题一起看下。感觉还有有些联系的。

今天最后一道,链表反转。

题目:

输入一个链表,反转链表后,输出链表的所有元素。

先给出优秀的代码:

public class Solution {
    public ListNode ReverseList(ListNode head) {
        if(head==null)
            return null;
        ListNode newHead = null;
        ListNode pNode = head;
        ListNode pPrev = null;
        while(pNode!=null){
            ListNode pNext = pNode.next;
            if(pNext==null)
                newHead = pNode;
            pNode.next = pPrev;
            pPrev = pNode;
            pNode = pNext;
        }
        return newHead;
    }
}

反正我没想出来这么写。我写的比较烂,此处略。这道题目不知道该如何总结。在下篇文章会结合剩下的几道题目总结一下。


_______________________________________________________________________________________________________________

分割线表示更新:

先更新一个题目答案,后来总结又写的一种答案(这是之前的思路,代码保留了,总结的时候又反思当时为什么没写出来,发现自己对题目的理解有误,包括对java对象的理解也有欠缺的地方,后面会指出之前的错误认识):

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


先更新下解法:

 public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        while (pHead1 != null) {
            ListNode tmp = pHead2;
            while (tmp != null) {
                if (pHead1.val == tmp.val && pHead1.next == tmp.next) {
                    System.out.println(pHead1.val);
                    return pHead1;
                }
                else {
                    tmp = tmp.next;
                } ;
            }
            pHead1 = pHead1.next;
        }

        return null;
    }

    @Test
    public void test() {
        ListNode node7 = new ListNode(7, null);
        ListNode node6 = new ListNode(3, node7);
        ListNode node5 = new ListNode(2, node6);
        ListNode node4 = new ListNode(-2, node5);

        ListNode node1 = new ListNode(7, node5);
        ListNode head = new ListNode(1, node1);

        findFirstCommonNode(head, node4);
    }

下面记录自己的纠正错误的过程及新的发现:

修改前的代码:

  public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {

        List<ListNode> list = new ArrayList<>();
        while (pHead1 != null) {
            list.add(pHead1);
            pHead1 = pHead1.next;
        }
        while (pHead2 != null) {
            if (list.contains(pHead2)) {
                System.out.println(pHead2.val);
                return pHead2;
            }
            pHead2 = pHead2.next;
        }
        return null;
    }

    @Test
    public void test() {
        ListNode node7 = new ListNode(7, null);
        ListNode node6 = new ListNode(3, node7);
        ListNode node5 = new ListNode(2, node6);
        ListNode node4 = new ListNode(-2, node5);
        ListNode node3 = new ListNode(4, null);
        ListNode node2 = new ListNode(3, node3);
        ListNode node1 = new ListNode(2, node2);
        ListNode head = new ListNode(1, node1);

        findFirstCommonNode(node1, node4);
    }

这里为什么要修改,是因为一旦两个链表出现公共结点后,则其next结点也是相同,其next之后的结点也都是相同。这里的相同是指,两个引用指向了同一块儿内存区域。

我个人给出公共结点的公式:

node1.val=node2.val
node1.next=node1.next

图示如下:


我最初的理解如下:


还有中错误理解这里也贴出来:


这种理解是错误的,因为在实际的JVM存储空间上,node33,node43是两块内存区,而不是仅仅指这两个结点的val是相等,下面展示下更有说服力的代码:

 public ListNode findFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        while (pHead1 != null) {
            ListNode tmp = pHead2;
            while (tmp != null) {
                if (pHead1.val == tmp.val && pHead1.next == tmp.next) {
                    System.out.println(pHead1.val);
                    return pHead1;
                }
                else {
                    tmp = tmp.next;
                } ;
            }
            pHead1 = pHead1.next;
        }

        return null;
    }

    @Test
    public void test() {
        ListNode node7 = new ListNode(7, null);
        ListNode node6 = new ListNode(3, node7);
        ListNode node5 = new ListNode(2, node6);
        ListNode node4 = new ListNode(-2, node5);

        ListNode node1 = new ListNode(7, node5);
        ListNode head = new ListNode(1, node1);

        findFirstCommonNode(head, node4);
    }

    @Test
    public void test1() {
        ListNode node24 = new ListNode(4, null);
        ListNode node23 = new ListNode(3, node24);
        ListNode node22 = new ListNode(9, node23);
        ListNode node21 = new ListNode(8, node22);

        ListNode node14 = new ListNode(4, null);
        ListNode node13 = new ListNode(3, node14);
        ListNode node12 = new ListNode(2, node13);
        ListNode node11 = new ListNode(1, node12);

        System.out.println(node23 == node13);

        findFirstCommonNode(node11, node21);
    }

    @Test
    public void test3() {
        ListNode node24 = new ListNode(4, null);
        ListNode node23 = new ListNode(3, node24);
        ListNode node22 = new ListNode(9, node23);
        ListNode node21 = new ListNode(8, node22);

        ListNode node14 = new ListNode(4, null);
        ListNode node13 = node23;
        ListNode node12 = new ListNode(2, node13);
        ListNode node11 = new ListNode(1, node12);

        System.out.println(node23 == node13);

        findFirstCommonNode(node11, node21);
    }
上面的输出:

false
4

true
3

很直观的看到,不同之处吧。test1中的代码,就是new 了node13和node23,他两不相等,test3中的测试代码node13和node23就是相等的。

图示:


这个问题,需要客官自己再查询关于java对象引用及new的相关知识结合理解。

如果看官理解上面的题目,接下来再来更新一道题目,会用到我们上面所说的内容。

题目描述:
 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),
 返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)
客官可以自己差试一下,单链表怎么写?
代码如下:
    public RandomListNode clone(RandomListNode pHead) {

        if (pHead == null) {
            return null;
        }
        RandomListNode node = new RandomListNode(pHead.label);
        node.next = pHead.next;
        node.random = pHead.random;

        pHead = pHead.next;
        RandomListNode tmp3 = node;
        while (pHead != null) {
            RandomListNode node2 = new RandomListNode(pHead.label);
            node2.next = pHead.next;
            node2.random = pHead.random;
            tmp3.next = node2;
            tmp3 = node2;
            pHead = pHead.next;
        }
        return node;
    }

    @Test
    public void test() {
        RandomListNode node6 = new RandomListNode(5, null, null);
        RandomListNode node5 = new RandomListNode(4, node6, null);
        RandomListNode node4 = new RandomListNode(4, node5, null);
        RandomListNode node3 = new RandomListNode(3, node4, null);
        RandomListNode node2 = new RandomListNode(3, node3, node6);
        RandomListNode node1 = new RandomListNode(2, node2, null);
        RandomListNode head = new RandomListNode(1, node1, node5);
        System.out.println("看下结果:" + clone(head).label);
    }
我个人就是先写单链表的,用自己能写出来的解法写的,后来一步步优化到这版的。这个题目有点儿,装逼的地方就是, 复杂链表。其实就是多了个属性,我才不管你是个啥。(可能是大多数人会使用C来操作,所以可能心里就会抵触,放弃了,我之前难道这个玩意儿,也是这TM是什么玩意儿)
第一版:
 public RandomListNode clone(RandomListNode pHead) {

        if (pHead == null) {
            return null;
        }
        RandomListNode node = new RandomListNode(pHead.label);
        node.next = pHead.next;
        node.random = pHead.random;
        pHead = pHead.next;
        Map<Integer, Map<String, Object>> map = new HashMap<>();
        String next = "next";
        String label = "label";
        String random = "random";
        int index = 0;
        while (pHead != null) {
            HashMap<String, Object> map2 = new HashMap<>();
            map2.put(next, pHead.next);
            map2.put(label, pHead.label);
            map2.put(random, pHead.random);
            map.put(index, map2);
            index++ ;
            pHead = pHead.next;
        }
        RandomListNode tmp3 = node;
        for (int i = 0; i < index; i++ ) {
            RandomListNode node2 = new RandomListNode((Integer)map.get(i).get(label));
            node2.next = (RandomListNode)map.get(i).get(next);
            node2.random = (RandomListNode)map.get(i).get(random);
            tmp3.next = node2;
            tmp3 = node2;
        }
        return node;
    }

个人在使用第三方结合,屡试屡爽,所以先用这个写一个版本出来,发现两个优化点:
 第一次尝试优化了for循环:

 第一个for循环将链表的结点取出来,连接方式没有改变,放到map里。
 第二个for循环干的事:利用for循环将其中的取出来,

我靠,先放进去再取出来,所以将其归并到一个for循环中。

第二次将map优化掉:
map.get(key)在某种意义上讲等价于Object.attribute。而且比map厉害的是,放入map中的key不能重复啊,但是对象用来取值,是没有这个问题的。

最大的心得,学会使用debug,一步步跟踪代码,最重要的是:
对于不会的,或者当时的思路没有做出来,可以放一放,或者参考别人的也行,但是当时自己的代码一定要保留。
一定不要留着疑问,不解决!
一定不要留着疑问,不解决!
一定不要留着疑问,不解决!

后续的会更新一篇总结,今天因为做复杂链表这道题,还有就是回看了另外一道题,没有总结。总结会画流程图,这个很刺激的。

完整代码地址点击这里

ps:更于2017.12.22 凌晨1.42。困死我了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值