面试算法:Linked List-链 表

来源:互联网 发布:java新闻发布管理系统 编辑:程序博客网 时间:2024/06/05 02:59

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