一种浅显易懂理解链表高频面试题的全新思路

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35440678/article/details/83443575

前言

我们都知道链表相关的算法题是笔试,面试的高频题,往往链表题的实现代码通常只有几行,但很多人被链表的指针指来指去,极容易绕晕。比如链表反转的一种实现:

  ListNode *Reverse(ListNode *pHead) {
        ListNode *pre = NULL;
        ListNode *cur = pHead;
        while (cur) {
            ListNode *tmp = cur->next;
            cur->next = pre;

            pre = cur;
            cur = tmp;
        }
        return pre;
    }

看到类似cur->next=pre;pre=cur;cur=tmp;这三行到底在整个链表内部是怎么转移的搞不清楚,看貌似看懂了,但自己写又写不出来。

根本原因

原因是什么?核心还是思考方法不对!我们理解链表题的逻辑时,通常,在我们脑海中只存在一条链表,每个指针只是在链表中的某一个位置,每个指针来回指向错综复杂,最终自己把自己搞晕,比如,理解上面链表反转的代码,脑中的思考过程是这样的:

// 初始链表:[1]->[2]->[3]->[4]
ListNode *Reverse(ListNode *pHead) {
    ListNode *pre = NULL;
    ListNode *cur = pHead;
    //一开始pre和cur的指向位置:[pre null] [cur 1] ->[2]->[3]->[4]
    while (cur) {
        ListNode *tmp = cur->next;
        cur->next = pre;
        //执行后: [cur 1]->[pre null] [tmp 2]->[3]->[4]
        
        pre = cur;
         //执行后:[pre 1]->[null]  [tmp 2]->[3]->[4]
        cur = tmp;
         //执行后: [cur 2]->[3]->[4]
    }
    return pre;
}

执行一次循环后,cur指向了下一个节点,但是pre指向哪了?pre怎么和cur关联的?整个反转后的链表是保持在哪个指针上?等等一系列疑问回荡在脑海中,挥之不去,看着好像很有道理,但是从头自己写又无从下手!

方法论

可以看到,整个思考过程逻辑很乱,我提出一种全新的方法叫:以指针为核心,而非链表本身。什么意思,就是关注每个辅助指针的变动关系,每个辅助指针为各自独立事件去关注指针本身的变化。看着有点拗口,举两个实例。

举个例子(链表反转):

链表反转这道题,一共定义了pre和cur两个辅助指针,那么以pre和cur指针的变动关系来去看逻辑,这是就没有链表本身没有关系了,只要时刻关注pre和cur两个指针的执行变化。

// 初始链表:[1]->[2]->[3]->[4]
 ListNode *Reverse(ListNode *pHead) {
    ListNode *pre = NULL;
    ListNode *cur = pHead;
    /* 循环前2个指针的状态:
    [pre null]  
    [cur 1] ->[2]->[3]->[4]
    */
     while (cur) {
        ListNode *tmp = cur->next;
         /* tmp== cur->next; 又多了一个tmp辅助指针,pre和cur没变化
         [pre null ]
         [tmp 2]->[3]->[4]
         [cur 1] ->[2]->[3]->[4]
         */
        cur->next = pre;
         /* 把pre赋值给cur->next,变化的只有cur,pre和tmp没变化
         [pre null ]
         [tmp 2]->[3]->[4]
         [cur 1] ->[pre null]
         */
        pre = cur;
         /*把cur赋值给tmp,变化的只有pre,cur和tmp没变化
         [pre 1 ]->[null]
         [tmp 2]->[3]->[4]
         [cur 1] ->[null]
         */
        cur = tmp;
        /*
         [pre 1 ]->[null]
         [tmp 2]->[3]->[4]
         [cur 2]->[3]->[4]
        */
    }
    return pre;
   }

以此循环,继续往下推演,就能得到最终反转的链表在pre辅助指针上。两个指针独立看变化过程:

  • pre指针:从一开始的null,变为等于[1],所以pre就是保持反转链表后的链表指针,每次循环都把pre等于最后一个元素,一直维持反转链表;
  • cur指针:一开始是原始链表本身,循环一次后讲链表头去掉(已经把链表头转移到pre头指针),每次循环少一个元素,知道cur为空结束;

再举一个例子(链表两两交换)

题目:

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
示例:

给定 1->2->3->4, 你应该返回 2->1->4->3.
说明:

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

思考过程: 定义了tmp、pre、cur三个辅助指针,看这3个指针的变化过程。

理解过程:


    ListNode *SwapPairs(ListNode *head) {
        if (head ==NULL) return head;
        ListNode tmp(0);
        tmp.next = head;
        ListNode *pre = &tmp;
        ListNode * cur = head;
            /*
             * 0、初始
             * [tmp pre] -> [cur 1]->[2] ->[3]->[4]
             * [pre] -> [1]->[2] ->[3]->[4]
             * [cur 1]-> [2] ->[3]->[4]
             */
        while (cur && cur->next){
            pre->next = cur->next;
             /*
             * 1、 只有两个指针   pre->next = cur->next;
             * [tmp pre 0] -> [2] ->[3]->[4]
             * [pre 0] -> [2] ->[3]->[4]
             * [cur 1] -> [2] ->[3]->[4]  没变
             */
            pre = pre->next;
            /*
             * 2、 pre = cur->next;
             * [tmp 0] -> [pre 2] ->[3]->[4]
             * [pre 2]->[3]->[4]
             * [cur 1]->[2]->[3]->[4]  没变
             */
            cur->next = pre->next;
            /*
             * 3、  cur->next = pre->next;
             * [tmp 0] -> [pre 2] ->[3]->[4] 没变
             * [pre 2] ->[3]->[4] 没变
             * [cur 1] ->[3]->[4]
             */
            pre->next = cur;
            /*
             * 4、pre->next = cur;
             * [tmp 0] -> [pre 2] ->[1]->[3]->[4]
             * [pre 2] ->[1]->[3]->[4]
             * [cur 1] ->[3]->[4]
             */
            pre = cur;
            /*
             * 5、 pre = cur;
             * [tmp 0] -> [2] ->[pre 1]->[3]->[4]
             * [pre 1] ->[3]->[4]
             * [cur 1] ->[3]->[4]
             */
            cur = cur->next;
            /*
             * 6、 cur = cur->next;
             * [tmp 0] -> [2] ->[pre 1]->[cur 3]->[4]
             * [pre 1] ->[3]->[4]
             * [cur 3] ->[4]
             */
        }
        return tmp.next;

    }

可以看到,

  • tmp指针:tmp初始化后从头到尾都没有赋值过,所以tmp保持交换后的链表;
  • pre指针:开始指向两两交换前一个元素,循环过程中指向第二个元素,然后让tmp指向自己;
  • cur指针:一开始指向交换第一个元素,循环过程中pre指向cur,这时完成两元素交换,最后两句完成下标后移;

思路的优点

以指针为核心++,而非链表 的理解思路最大的好处是,你可以独立观察每个辅助指针充当的角色,每个角色各司其职,最终一起完成整个算法逻辑的实现。

猜你喜欢

转载自blog.csdn.net/qq_35440678/article/details/83443575