數據結構和算法面試題系列—鏈表

這個系列是我多年前找工做時對數據結構和算法總結,其中有基礎部分,也有各大公司的經典的面試題,最先發布在CSDN。現整理爲一個系列給須要的朋友參考,若有錯誤,歡迎指正。本系列完整代碼地址在 這裏html

0 概述

鏈表做爲一種基礎的數據結構,在不少地方會用到。如在Linux內核代碼,redis源碼,python源碼中都有使用。除了單向鏈表,還有雙向鏈表,本文主要關注單向鏈表(含部分循環鏈表題目,會在題目中註明,其餘狀況都是討論簡單的單向鏈表)。雙向鏈表在redis中有很好的實現,也在個人倉庫中拷貝了一份用於測試用,本文的相關代碼在 這裏node

1 定義

先定義一個單向鏈表結構,以下,定義了鏈表結點和鏈表兩個結構體。這裏我沒有多定義一個鏈表的結構體,保存頭指針,尾指針,鏈表長度等信息,目的也是爲了多練習下指針的操做。python

// aslist.h

// 鏈表結點定義
typedef struct ListNode {
    struct ListNode *next;
    int value;
} listNode;
複製代碼

鏈表結構

2 基本操做

在上一節的鏈表定義基礎上,咱們完成幾個基本操做函數,包括鏈表初始化,鏈表中添加結點,鏈表中刪除結點等。git

/**
 * 建立鏈表結點
 */
ListNode *listNewNode(int value)
{
    ListNode *node;
    if (!(node = malloc(sizeof(ListNode))))
        return NULL;

    node->value = value;
    node->next = NULL;
    return node;
}

/**
 * 頭插法插入結點。
 */
ListNode *listAddNodeHead(ListNode *head, int value)
{
    ListNode *node;
    if (!(node = listNewNode(value)))
        return NULL;

    if (head) 
        node->next = head;

    head = node;
    return head;
}

/**
 * 尾插法插入值爲value的結點。
 */
ListNode *listAddNodeTail(ListNode *head, int value)
{
    ListNode *node;
    if (!(node = listNewNode(value)))
        return NULL;

    return listAddNodeTailWithNode(head, node);
}

/**
 * 尾插法插入結點。
 */
ListNode *listAddNodeTailWithNode(ListNode *head, ListNode *node)
{
    if (!head) {
        head = node;
    } else {
        ListNode *current = head;
        while (current->next) {
            current = current->next;
        } 
        current->next = node;
    }
    return head;
}

/**
 * 從鏈表刪除值爲value的結點。
 */
ListNode *listDelNode(ListNode *head, int value)
{
    ListNode *current=head, *prev=NULL;

    while (current) {
        if (current->value == value) {
            if (current == head)
                head = head->next;

            if (prev)
                prev->next = current->next;

            free(current);
            break;
        }

        prev = current;
        current = current->next;
    }
    return head;
}

/**
 * 鏈表遍歷。
 */
void listTraverse(ListNode *head)
{
    ListNode *current = head;
    while (current) {
        printf("%d", current->value);
        printf("->");
        current = current->next;
        if (current == head) // 處理首尾循環鏈表狀況
            break;
    }

    printf("NULL\n");
}

/**
 * 使用數組初始化一個鏈表,共len個元素。
 */
ListNode *listCreate(int a[], int len)
{
    ListNode *head = NULL;
    int i;
    for (i = 0; i < len; i++) {
        if (!(head = listAddNodeTail(head, a[i])))
            return NULL;
    }
    return head;
}

/**
* 鏈表長度函數
*/
int listLength(ListNode *head)
{
    int len = 0;
    while (head) {
        len++;
        head = head->next;
    }
    return len;
}
複製代碼

3 鏈表相關面試題

3.1 鏈表逆序

題: 給定一個單向鏈表 1->2->3->NULL,逆序後變成 3->2->1->NULLgithub

解: 常見的是用的循環方式對各個結點逆序鏈接,以下:面試

/**
 * 鏈表逆序,非遞歸實現。
*/
ListNode *listReverse(ListNode *head)
{
    ListNode *newHead = NULL, *current = head;
    while (current) {
        ListNode *next = current->next;
        current->next = newHead;
        newHead = current;
        current = next;
    }

    return newHead;
}
複製代碼

若是帶點炫技性質的,那就來個遞歸的解法,以下:redis

/**
 * 鏈表逆序,遞歸實現。
 */
ListNode *listReverseRecursive(ListNode *head)
{
    if (!head || !head->next) {
        return head;
    }

    ListNode *reversedHead = listReverseRecursive(head->next);
    head->next->next = head;
    head->next = NULL;
    return reversedHead;
}
複製代碼

3.2 鏈表複製

題: 給定一個單向鏈表,複製並返回新的鏈表頭結點。算法

解: 一樣能夠有兩種解法,非遞歸和遞歸的,以下:數組

/**
 * 鏈表複製-非遞歸
 */
ListNode *listCopy(ListNode *head) 
{
    ListNode *current = head, *newHead = NULL, *newTail = NULL; 
    while (current) {
        ListNode *node = listNewNode(current->value);
        if (!newHead) { // 第一個結點
            newHead = newTail = node;
        } else {
            newTail->next = node;
            newTail = node;
        }
        current = current->next;
    }
    return newHead;
}
	
/**
 * 鏈表複製-遞歸
 */
ListNode *listCopyRecursive(ListNode *head)
{
    if (!head) 
        return NULL;
	
    ListNode *newHead = listNewNode(head->value);
    newHead->next = listCopyRecursive(head->next);
    return newHead;
}
複製代碼

3.3 鏈表合併

題: 已知兩個有序單向鏈表,請合併這兩個鏈表,使得合併後的鏈表仍然有序(注:這兩個鏈表沒有公共結點,即不交叉)。如鏈表1是 1->3->4->NULL,鏈表2是 2->5->6->7->8->NULL,則合併後的鏈表爲 1->2->3->4->5->6->7->8->NULLbash

解: 這個很相似歸併排序的最後一步,將兩個有序鏈表合併到一塊兒便可。使用2個指針分別遍歷兩個鏈表,將較小值結點歸併到結果鏈表中。若是一個鏈表歸併結束後另外一個鏈表還有結點,則把另外一個鏈表剩下部分加入到結果鏈表的尾部。代碼以下所示:

/**
 * 鏈表合併-非遞歸
 */
ListNode *listMerge(ListNode *list1, ListNode *list2)
{
    ListNode dummy; // 使用空結點保存合併鏈表
    ListNode *tail = &dummy;

    if (!list1)
        return list2;

    if (!list2)
        return list1;

    while (list1 && list2) {
        if (list1->value <= list2->value) {
            tail->next = list1;
            tail = list1;
            list1 = list1->next;
        } else {
            tail->next = list2;
            tail = list2;
            list2 = list2->next;
        }
    }

    if (list1) {
        tail->next = list1;
    } else if (list2) {
        tail->next = list2;
    }

    return dummy.next;
}
複製代碼

固然,要實現一個遞歸的也不難,代碼以下:

ListNode *listMergeRecursive(ListNode *list1, ListNode *list2)
{
    ListNode *result = NULL;

    if (!list1)
        return list2;

    if (!list2)
        return list1;

    if (list1->value <= list2->value) {
        result = list1;
        result->next = listMergeRecursive(list1->next, list2);
    } else {
        result = list2;
        result->next = listMergeRecursive(list1, list2->next);
    }

    return result;
}
複製代碼

3.4 鏈表相交判斷

題: 已知兩個單向鏈表list1,list2,判斷兩個鏈表是否相交。若是相交,請找出相交的結點。

解1: 能夠直接遍歷list1,而後依次判斷list1每一個結點是否在list2中,可是這個解法的複雜度爲 O(length(list1) * length(list2))。固然咱們能夠遍歷list1時,使用哈希表存儲list1的結點,這樣再遍歷list2便可判斷了,時間複雜度爲O(length(list1) + length(list2)),空間複雜度爲 O(length(list1)),這樣相交的結點天然也就找出來了。固然,找相交結點還有更好的方法。

解2: 兩個鏈表若是相交,那麼它們從相交後的節點必定都是相同的。假定list1長度爲len1,list2長度爲len2,且 len1 > len2,則咱們只須要將 list1 先遍歷 len1-len2個結點,而後兩個結點一塊兒遍歷,若是遇到相等結點,則該結點就是第一個相交結點。

/**
 * 鏈表相交判斷,若是相交返回相交的結點,不然返回NULL。
 */
ListNode *listIntersect(ListNode *list1, ListNode *list2)
{
    int len1 = listLength(list1);
    int len2 = listLength(list2);
    int delta = abs(len1 - len2);

    ListNode *longList = list1, *shortList = list2;

    if (len1 < len2) {
        longList = list2;
        shortList = list1;
    }

    int i;
    for (i = 0; i < delta; i++) {
        longList = longList->next;
    }

    while (longList && shortList) {
        if (longList == shortList)
            return longList;

        longList = longList->next;
        shortList = shortList->next;
    }

    return NULL;
}
複製代碼

3.5 判斷鏈表是否存在環

題: 給定一個鏈表,判斷鏈表中是否存在環。

判斷鏈表環

解1: 容易想到的方法就是使用一個哈希表記錄出現過的結點,遍歷鏈表,若是一個結點重複出現,則表示該鏈表存在環。若是不用哈希表,也能夠在鏈表結點 ListNode 結構體中加入一個 visited字段作標記,訪問過標記爲1,也同樣能夠檢測。因爲目前咱們尚未實現一個哈希表,這個方法代碼後面再加。

解2: 更好的一種方法是 Floyd判圈算法,該算法最先由羅伯特.弗洛伊德發明。經過使用兩個指針fast和slow遍歷鏈表,fast指針每次走兩步,slow指針每次走一步,若是fast和slow相遇,則表示存在環,不然不存在環。(注意,若是鏈表只有一個節點且沒有環,不會進入while循環)

/**
 * 檢測鏈表是否有環-Flod判圈算法
 * 若存在環,返回相遇結點,不然返回NULL
 */
ListNode *listDetectLoop(ListNode *head)
{
    ListNode *slow, *fast;
    slow = fast = head;

    while (slow && fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            printf("Found Loop\n");
            return slow;
        }
    }

    printf("No Loop\n");
    return NULL;
}

void testListDetectLoop()
{
    printf("\nTestListDetectLoop\n");
    int a[] = {1, 2, 3, 4};
    ListNode *head = listCreate(a, ALEN(a));
    listDetectLoop(head);

    // 構造一個環
    head->next->next->next = head;
    listDetectLoop(head);
}
複製代碼

擴展: 檢測到有環的話,那要如何找鏈表的環的入口點呢?

首先,咱們來證實一下爲何上面的解2提到的算法是正確的。若是鏈表不存在環,由於快指針每次走2步,必然會比慢指針先到達鏈表尾部,不會相遇。

若是存在環,假定快慢指針通過s次循環後相遇,則此時快指針走的距離爲 2s,慢指針走的距離爲 s,假定環內結點數爲r,則要相遇則必須知足下面條件,即相遇時次數知足 s = nr。即從起點以後下一次相遇須要循環 r 次。

2s - s = nr => s = nr
複製代碼

以下圖所示,環長度r=4,則從起點後下一次相遇須要通過4次循環。

那麼環的入口點怎麼找呢?前面已經可知道第一次相遇要循環 r 次,而相遇時慢指針走的距離爲 s=r,設鏈表總長度爲L,鏈表頭到環入口的距離爲a,環入口到相遇點的距離爲x,則L = a + r,能夠推導出 a = (L-x-a),其中L-x-a爲相遇點到環入口點的距離,即鏈表頭到環入口的距離a等於相遇點到環入口距離

s = r = a + x => a + x = (L-a) => a = L-x-a
複製代碼

因而,在判斷鏈表存在環後,從相遇點和頭結點分別開始遍歷,兩個指針每次都走一步,當兩個指針相等時,就是環的入口點。

/**
 * 查找鏈表中環入口
 */
ListNode *findLoopNode(ListNode *head)
{
    ListNode *meetNode = listDetectLoop(head);
    if (!meetNode)
        return NULL;

    ListNode *headNode = head;
    while (meetNode != headNode) {
        meetNode = meetNode->next;
        headNode = headNode->next;
    }
    return meetNode;
}
複製代碼

3.6 鏈表模擬加法

題: 給定兩個鏈表,每一個鏈表的結點值爲數字的各位上的數字,試求出兩個鏈表所表示數字的和,並將結果以鏈表形式返回。假定兩個鏈表分別爲list1和list2,list1各個結點值分別爲數字513的個位、十位和百位上的數字,同理list2的各個結點值爲數字295的各位上的數字。則這兩個數相加爲808,因此輸出按照從個位到百位順序輸出,返回的結果鏈表以下。

list1:  (3 -> 1 -> 5 -> NULL)

list2:  (5 -> 9 -> 2 -> NULL)

result: (8 -> 0 -> 8 -> NULL)
複製代碼

解: 這個題目比較有意思,須要對鏈表操做比較熟練。咱們考慮兩個數字相加過程,從低位到高位依次相加,若是有進位則標記進位標誌,直到最高位才終止。設當前位的結點爲current,則有:

current->data = list1->data + list2->data + carry
(其中carry爲低位的進位,若是有進位爲1,不然爲0)
複製代碼

非遞歸代碼以下:

/**
 * 鏈表模擬加法-非遞歸解法
 */
ListNode *listEnumarateAdd(ListNode *list1, ListNode *list2)
{
    int carry = 0;
    ListNode *result = NULL;

    while (list1 || list2 || carry) {
        int value = carry;
        if (list1) {
            value += list1->value;
            list1 = list1->next;
        }

        if (list2) {
            value += list2->value;
            list2 = list2->next;
        }

        result = listAddNodeTail(result, value % 10);
        carry = ( value >= 10 ? 1: 0);
    }

    return result;
}
複製代碼

非遞歸實現以下:

/**
 * 鏈表模擬加法-遞歸解法
 */
ListNode *listEnumarateAddRecursive(ListNode *list1, ListNode *list2, int carry)
{
    if (!list1 && !list2 && carry==0)
        return NULL;

    int value = carry;
    if (list1)
        value += list1->value;

    if (list2)
        value += list2->value;

    ListNode *next1 = list1 ? list1->next : NULL;
    ListNode *next2 = list2 ? list2->next : NULL;
    ListNode *more = listEnumarateAddRecursive(next1, next2, (value >= 10 ? 1 : 0));
    ListNode *result = listNewNode(carry);
    result->value = value % 10;
    result->next = more;

    return result;
}
複製代碼

3.7 有序單向循環鏈表插入結點

題: 已知一個有序的單向循環鏈表,插入一個結點,仍保持鏈表有序,以下圖所示。

循環鏈表

解: 在解決這個問題前,咱們先看一個簡化版本,就是在一個有序無循環的單向鏈表中插入結點,仍然保證其有序。這個問題的代碼相信多數人都很熟悉,通常都是分兩種狀況考慮:

  • 1)若是原來鏈表爲空或者插入的結點值最小,則直接插入該結點並設置爲頭結點。
  • 2)若是原來鏈表非空,則找到第一個大於該結點值的結點,並插入到該結點的前面。若是插入的結點值最大,則插入在尾部。

實現代碼以下:

/**
 * 簡化版-有序無循環鏈表插入結點
 */
ListNode *sortedListAddNode(ListNode *head, int value)
{
    ListNode *node = listNewNode(value);
    if (!head || head->value >= value) { //狀況1
        node->next = head;
        head = node;
    } else {  //狀況2
        ListNode *current = head;
        while (current->next != NULL && current->next->value < value)
            current = current->next;
        node->next = current->next;
        current->next = node;
    }
    return head;
}
複製代碼

固然這兩種狀況也能夠一塊兒處理,使用二級指針。以下:

/**
 * 簡化版-有序無循環鏈表插入結點(兩種狀況一塊兒處理)
 */
void sortedListAddNodeUnify(ListNode **head, int value)
{
    ListNode *node = listNewNode(value);
    ListNode **current = head;
    while ((*current) && (*current)->value < value) {
        current = &((*current)->next);
    }
    node->next = *current;
    *current = node;
}
複製代碼

接下來看循環鏈表的狀況,其實也就是須要考慮下面2點:

  • 1) prev->value ≤ value ≤ current->value: 插入到prev和current之間。
  • 2) value爲最大值或者最小值: 插入到首尾交接處,若是是最小值從新設置head值。

代碼以下:

/**
 * 有序循環鏈表插入結點
 */
ListNode *sortedLoopListAddNode(ListNode *head, int value)
{
    ListNode *node = listNewNode(value);
    ListNode *current = head, *prev = NULL;
    do {
        prev = current;
        current = current->next;
        if (value >= prev->value && value <= current->value)
            break;
    } while (current != head);

    prev->next = node;
    node->next = current;

    if (current == head && value < current->value) // 判斷是否要設置鏈表頭
        head = node;

    return head;
}
複製代碼

3.8 輸出鏈表倒數第K個結點

題: 給定一個簡單的單向鏈表,輸出鏈表的倒數第K個結點。

解1: 若是是順數第K個結點,不用多思考,直接遍歷便可。這個題目的新意在於它是要輸出倒數第K個結點。一個直觀的想法是,假定鏈表長度爲L,則倒數第K個結點就是順數的 L-K+1 個結點。如鏈表長度爲3,倒數第2個,就是順數的第2個結點。這樣須要遍歷鏈表2次,一次求長度,一次找結點。

/**
* 鏈表倒數第K個結點-遍歷兩次算法
*/
ListNode *getLastKthNodeTwice(ListNode *head, int k)
{
    int len = listLength(head);     
    if (k > len)
        return NULL;

    ListNode *current = head; 
    int i;
    for (i = 0; i < len-k; i++)  //遍歷鏈表,找出第N-K+1個結點
        current = current->next;

    return current;
}
複製代碼

解2: 固然更好的一種方法是遍歷一次,設置兩個指針p1,p2,首先p1和p2都指向head,而後p2向前走k步,這樣p1和p2之間就間隔k個節點。最後p1和p2同時向前移動,p2走到鏈表末尾的時候p1恰好指向倒數第K個結點。代碼以下:

/**
* 鏈表倒數第K個結點-遍歷一次算法
*/
ListNode *getLastKthNodeOnce(ListNode *head, int k)
{
    ListNode *p1, *p2;
    p1 = p2 = head;

    for(; k > 0; k--) {
        if (!p2) // 鏈表長度不夠K
            return NULL;
        p2 = p2->next;
    }

    while (p2) {
        p1 = p1->next;
        p2 = p2->next;
    }
    return p1;
}
複製代碼

參考資料

相關文章
相關標籤/搜索