面试算法:Linked List-链 表

很多面试相关的书籍都有链表和字符串操作相关的题,这类问题更考验编程基本功和对基础知识的掌握情况。由于之前很多题目做过就忘,再做时总是磕磕绊绊,说明没有真正消化。于是想到把做过的题整理出来,写上自己的一些做题感悟,多多总结常用的方法和技巧。如果能把问题向别人讲明白,那才说明真的理解了。本文中的部分代码参考了网上一些源码的写法,且在 leetcode 上测试通过。

链表常用方法和技巧

  • Dummy Node
    哑节点的使用针对单链表没有前向指针的问题。保证head节点不会在删除操作中丢失。当head有变化(比如被删除或被修改),或者涉及到边界判断时,使用Dummy Node可以很好的简化代码。

  • Fast and slow pointer
    快慢指针的快和慢指的是指针向后移动的步长。每次移动步长大的叫快指针,步长小的即为慢指针。

    快慢指针有如下应用:

    • 寻找未知长度的特殊位置的某个节点,比如中间节点、倒数第k个节点。其中,寻找中间节点可以设置快指针*fast移动速度是慢指针*slow的两倍,两个指针都从单链表的头节点出发,当*fast指向尾节点时,*slow正好走到中间了。寻找倒数第k个节点时,可以让*fast指针先出发,移动k-1步,然后同时移动*fast*slow指针,当*fast指针移动到尾节点时,*slow指针正好移动到倒数第k个节点。
    • 判断单链表是否存在环。同样设置两个指针 *fast*slow都指向单链表的头节点,其中*fast的移动速度是*slow的两倍。如果*fast = NULL,说明该单链表以NULL结尾,不存在环;如果 *fast = *slow,则快指针追上慢指针,说明该链表存在环。

以下各问题会用到下面的单链表定义:

// Definition for singly-Linked List
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(NULL) {}
};

在O(1)时间删除链表节点

  • 题目描述
    237. Delete Node in a Linked List

    Write a function to delete a node (except the tail) in a singly linked list, given only access to that node.

    Supposed the linked list is 1 -> 2 -> 3 -> 4 and you are given the third node with value 3, the linked list should become 1 -> 2 -> 4 after calling your function.

  • 思路
    用该节点的后继节点覆盖当前节点,然后删除下一个节点即可。该节点不能是尾节点。

  • C++

void deleteNode (ListNode *cur)
{
    assert(cur != NULL);
    assert(cur->next != NULL);  //不能是尾节点
    ListNode* p = cur->next;
    cur->val = p->val;
    cur->next = p->next;
    delete p;
}

反转链表

  • 题目描述
    206. Reverse Linked List

    Reverse a singly linked list.

  • 思路

    1. 迭代。迭代的本质是遍历,迭代的外部操作是循环。对于每一次的循环,先把当前节点head指向直接后继的指针改为指向其直接前驱pre,为此需先记录其直接后继head->next的值。然后把pre的值修改为head的值,把head的值修改为记录过的后继值,一步一步迭代下去。

    2. 递归。类似于栈,遇到终止条件则返回到上一层递归的调用处,然后继续向下执行,执行完成后返回该层的再上一层,就这样一层一层地返回。递归的执行顺序类似于深度优先遍历。一边递归一边修改指针指向,递归完成时,翻转也完成。

    3. 这里运用到链表操作中一个重要的技巧:Dummy Node。 设置一个哑节点dummy,让dummynext指针指向链表的第一个节点。每次循环时把head后面的head->next节点插入到哑节点后面。

      以第一次循环为例,把节点2插入到dummy之后,1之前。
      reverseList
      第二次循环时把3插入到dummy之后,2之前。依此类推。
      由于每次循环后链表的第一个节点都会变化,所以要先记录它的值,以便后续操作中在它的前面插入。

  • C++ - iteration

ListNode* reverseList(ListNode* head) 
{
    if (head == NULL || head->next == NULL) {
        return head;
    }
    ListNode *pre = NULL, *next;
    while (head != NULL) {
        next = head->next;
        head->next = pre;
        pre = head;
        head = next;
    }
    return pre;
}
  • 边界条件
    当最后headNULL时,整个翻转才完成。

  • C++ - recursion

ListNode* reverseList(ListNode* head) 
{
    if (head == NULL || head->next == NULL) {
        return head;
    }
    ListNode *newHead = reverseList(head->next);
    head->next->next = head;  
    head->next = NULL;
    return newHead;
}
  • 边界条件
    head == NULL:判断异常。
    head->next == NULL:递归终止。

  • C++ - Dummy Node

ListNode* reverseList(ListNode* head)
{
    ListNode dummy(0);
    dummy.next = head;
    ListNode* p = &dummy;
    while (head && head->next) {
        ListNode *temp = dummy.next; //记录链表的第一个节点
        p->next = head->next;
        head->next = p->next->next;
        p->next->next = temp;
    }
    return dummy.next;
}
  • 边界条件
    head为空时,不执行循环返回为空值。当head->next为空时,说明到了最后一个节点,退出循环。

判断单链表是否存在环

  • 题目描述
    141. Linked List Cycle

    Given a linked list, determine if it has a cycle in it.
    Follow up:
    Can you solve it without using extra space?

  • 思路

    1. set设访问标记。把单链表的节点一个一个放入set,判断节点是否已经存在于set中。如果set中已存在该节点,说明单链表有环。

    2. 使用快慢指针fastslow。两个指针都从表头开始往后走。设置slow每次走一步,fast每次走两步。如果fast遇到null,说明没有环,返回false。如果slow=fast,说明有环。时间复杂度为 O(n)

  • C++ -set

bool hasCycle(ListNode* head)
{
    set<ListNode*> visited;
    while (head) {
        set<ListNode*>::iterator iter = visited.find(head);
        if (iter == iter.end()) {
            visited.insert(head);

        } else {
            return true;
        }
        head = head->next;
    }
    return false;
}
  • C++ -Fast and slow pointer
bool hasCycle(ListNode* head)
{
    ListNode *fast, *slow;
    fast = slow = head;
    while (fast && fast->next) {
        fast = fast->next->next;
        slow = slow->next;
        if (fast == slow) {
            return true;
        }
    }
    return false;
}

扩展问题:单链表环的入口及长度

  • 题目描述
    142. Linked List Cycle II
    Given a linked list, return the node where the cycle begins. If there is no cycle, return null.
    Note: Do not modify the linked list.

    Follow up:
    Can you solve it without using extra space?
  • 思路
    hasCyle
    如上图:
    slow走到相遇点时走过的距离为 distslow=x+y
    fast走到相遇点时走过的距离为 distfast=(x+y+z)+y
    由于fast的速度是slow的2倍,所以 distfast=2distslow ,即 2(x+y)=(x+y+z)+y ,可知 x=z

    当快慢指针到达相遇点时:
    ①. 只需让fast指针继续从相遇点出发,slow指针从表头出发,并保持相同的速度,当它们再次相遇时,即是环的入口。
    ②. 让fast指针继续以两倍于slow指针的速度前进,记录各自走过的距离。当它们再次相遇时,fast刚好比slow多跑一圈,此时用fast走过的距离减去slow走过的距离即可得到环的长度。

  • C++ -Fast and slow pointer

 ListNode *detectCycle(ListNode *head) 
 {
    bool hasCycle = false;
    ListNode *fast, *slow;
    fast = slow = head;
    while (fast->next && fast->next->next) {
        fast = fast->next->next;
        slow = slow->next;
        if (fast == slow) {
            hasCycle = true;
            break;
        }
    }
    if (hasCycle == false) {
        return NULL;

    } else {
        slow = head;
        while (slow != fast) {
            slow = slow->next;
            fast = fast->next;
        }
    }
    return slow;
}

链表倒数第k个节点

设置前后两个指针precur指向同一个起点,让前一个指针先走k-1步,然后两个指针再同时一步一步移动,当cur走到最后一个节点时,此时pre正好指向倒数第k个节点。

  • 题目描述
    19. Remove Nth Node From End of List

    Given a linked list, remove the nth node from the end of list and return its head.

    For example,

    Given linked list: 1->2->3->4->5, and n = 2. After removing the second node from the end, the linked list becomes 1->2->3->5.

  • 思路
    运用上面的方法,使用前后两个指针。由于要删除第k个节点,当后面一个指针移动到尾节点时,前一个指针应移动到倒数第k个节点的前一个节点。然后pre->next = pre->next->next

  • C++ -Fast and slow pointer

ListNode* removeNthFromEnd(ListNode* head, int n) 
{
    ListNode dummy(0);
    dummy.next = head;
    ListNode *pre = &dummy;
    while (--n) {
        head = head->next;
    }
    while (head->next) { /*移动head指针到尾节点*/
        head = head->next;
        pre = pre->next;
    }
    pre->next = pre->next->next;
    return dummy.next;
}
  • 边界条件
    这个题要格外注意边界条件,应注意链表中只有一个节点的情况。不然很容易出错,或者把代码写复杂。
    为了使代码更简洁,减少不必要的边界判断,可以在表头前添加一个哑节点。让前一个指针pre指向哑节点,让后一个指针移动k-1步后再同时移动两个指针,当后一个指针移动到尾节点时,pre指针刚好移动到倒数第k个节点的前一个节点。而且这种做法保证了当链表中只有一个节点时,不必担心pre->next->next的越界问题。

扩展问题:求链表的中间节点

  • 题目描述
    148. Sort List
    Sort a linked list in O(nlogn) time using constant space complexity.
  • 思路
    排序时间复杂度为 O(nlogn) 的算法能想到快速排序和归并排序。排序时只需改变指针,所以不必像数组那样需要额外的存储空间,空间复杂度为 O(1) 。找中间节点只需 O(n2) 的线性时间,所以总时间复杂度为 O(nlogn) ,满足题目要求。

  • C++ -Fast and slow pointer

ListNode* sortList(ListNode* head) 
{
    if (head == NULL || head->next == NULL) {
        return head;
    }
    ListNode *slow, *fast;
    slow = fast = head;
    while (fast->next && fast->next->next) {
        fast = fast->next->next;
        slow = slow->next;
    }
    ListNode* mid = slow->next;
    slow->next = NULL;  //把一段链表分为两段子链表
    ListNode* list1 = sortList(head);
    ListNode* list2 = sortList(mid);
    return mergeList(list1, list2);
}

ListNode* mergeList(ListNode* list1, ListNode* list2)
{
    ListNode dummy(0);
    dummy.next = list1;
    ListNode *p = &dummy;
    while (NULL != list1 && NULL != list2) {
        if (list1->val <= list2->val) {
            p->next = list1;
            list1 = list1->next;

        } else {
            p->next = list2;
            list2 = list2->next;
        }
        p = p->next;
    }
    if (NULL != list1) {
        p->next = list1;

    } else {
        p->next = list2;
    }
    return dummy.next;
}
  • 边界条件
    求中间节点时,一时脑抽把while中的循环条件写成了fast && fast->next,结果一直RE。可见边界很重要啊,对循环中涉及到fast->next->next的操作一定要格外小心,不然很容易就越界啊各种错误。

判断两个链表是否相交

  • 题目描述
    160. Intersection of Two Linked Lists

    Write a program to find the node at which the intersection of two singly linked lists begins.

    For example, the following two linked lists:

    interSection
    begin to intersect at node c1.

  • 思路
    如果两个链表长度相同,只要一个一个地对比下去就能找到。当链表长度不同时,只需要从较长的链表头结点往下遍历,直到与短链表长度相同时再一个一个地比对即可。

  • C++

ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) 
{
    if (headA == NULL || headB == NULL) {
        return NULL;
    }
    int lenA = 1, lenB = 1;
    ListNode *pA = headA, *pB = headB;
    while (pA->next != NULL) {
        lenA++;
        pA = pA->next;
    }
    while (pB->next != NULL) {
        lenB++;
        pB = pB->next;
    }

    if (pA != pB) {
        return NULL;
    }
    if (lenA > lenB) {
        for (int i = 0; i < lenA - lenB; i++) {
            headA = headA->next;
        }

    } else {
        for (int i = 0; i < lenB - lenA; i++) {
            headB = headB->next;
        }
    }
    while (headA != headB) {
        headA = headA->next;
        headB = headB->next;
    }
    return headA;
}

Reference

面试精选:链表问题集锦
链表的反转问题:递归和非递归方式
求有环单链表中的环长、环起点、链表长
Proof of detecting the start of cycle in linked list

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值