《劍指offer》Java學習錄:鏈表(面試題5:從尾到頭打印鏈表)

鏈表


和數組不同,鏈表是一種動態的數據結構,在創建時並不需要知道他的長度。鏈表的結構很簡單,它通過指針(C/C++中)或者引用(Java中)將若干個節點連接成鏈狀結構。

在鏈表中插入一個節點時,我們只需要爲新節點分配內存,然後調整指針或引用的指向即可。因爲內存是在使用過程中動態分配,不會出現空閒內存得不到利用的情況,因此鏈表的空間效率比數組高。

C/C++中的鏈表

在C/C++中,單向鏈表節點定義如下:

struct ListNode {
    int mValue;
    ListNode *m_pNext;
};

而添加節點到鏈表中的函數如下:

void add2Tail(ListNode** pHead, int value) {
    ListNode *pNew = new ListNode();
    pNew->mValue = value;
    pNew->m_pNext = NULL;

    if (*pHead == NULL) {
        *pHead = pNew;
    } else {
        ListNode *pNode = *pHead;
        while(pNode->m_pNext != NULL) {
            pNode = pNode->m_pNext;
        }
        pNode->m_pNext = pNew;
    }
}

空鏈表意味着一個節點也沒有,代表鏈表的指針值爲NULL,爲了便於向空鏈表中添加節點,我們需要該錶鏈表指針指向新增的第一個節點位置,所以,在void add2Tail(ListNode** pHead, int value)函數中使用了雙指針。

因爲鏈表中,節點的內存不能確保連續,因此想要查找第i個節點,只能從頭結點開始,沿指針往下遍歷鏈表,時間效率爲O(n)O_{(n)},而數組的時間效率爲O(1)O_{(1)}。下面是查找到第一個含有某值的節點,並刪除的代碼。

void removeNode(ListNode **pHead, int value) {
    if (pHead == NULL || *pHead == NULL) {
        return;
    }

    ListNode *pDeleteNode = NULL;
    if ((*pHead)->mValue == value) {
        pDeleteNode = *pHead;
        *pHead = (*pHead)->m_pNext;
    } else {
        ListNode *pNode = *pHead;
        while (pNode->m_pNext != NULL && pNode->m_pNext->mValue != value) {
            pNode = pNode->m_pNext;
            if (pNode->m_pNext != NULL && pNode->m_pNext->mValue == value) {
                pDeleteNode = pNode->m_pNext;
                pNode->m_pNext = pNode->m_pNext->m_pNext;
            }
        }
    }

    if (pDeleteNode != NULL) {
        delete pDeleteNode;
        pDeleteNode = NULL;
    }
}

Java中的鏈表

Java中以鏈表爲底層結構的是集合框架中的LinkedListLinkedList中的鏈表爲雙向鏈表,來看看它的鏈表節點實現:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

每個節點都會保存前後節點的引用,雙向鏈表對於單向表的優勢在於可以雙向搜索。

在鏈表末尾添加元素:

/**
 * Links e as last element.
 */
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

鏈表對於數組的優勢在於可以高效的刪除或者替換節點,刪除或者替換時,只需要將關聯的指針或者引用替換到下一個節點就行了。

因爲LinkedList關於鏈表的操作封裝的足夠強大,所以在Java中,很少會見到自己實現鏈表。對於面向對象的語言來說,如非必要,儘量使用已有API是最大的美德。

但因爲單鏈表相對於其他數據結構來說,實現簡單,可以在20行代碼內實現,這對於不長的面試時間來說十分合適,所以經常能在面試中遇到。

面試題 5:從尾到頭打印鏈表

題目

輸入一個鏈表的頭結點,從尾到頭反向打印出每個節點的值。

鏈表節點定義如下:

struct ListNode {
    int m_Key;
    ListNode *m_pNext;
};

分析

第一種:因爲是單向鏈表,只能從頭到尾遍歷,但題卻要求反向遍歷。也就是第一個被遍歷的節點最後打印,最後一個遍歷的節點第一個打印,典型的後進先出。首先想到用棧來存儲遍歷節點,最後從棧中輸出。

第二種:說到棧結構的話,自然能想到我們的函數調用棧也是一種後進先出的模型。利用這一點,也可以用遞歸來解題。但在使用遞歸時需要注意,鏈表的長度並不固定,如果太長,將會使函數棧嵌套過深導致棧溢出。

鑑於遞歸會導致棧溢出的問題,不管是在面試還是在實際編碼過程中都需要謹慎使用。其實上面兩種解法,通常都是可以替換的。

解:C++

第一種解法:顯示棧解

void printListReverse_1(ListNode *pHead) {
    std::stack<ListNode *> nodes;

    ListNode *pNode = pHead;
    while (pNode != NULL) {
        nodes.push(pNode);
        pNode = pNode->m_pNext;
    }
    while (!nodes.empty()) {
        pNode = nodes.top();
        cout << pNode->m_Key << " ";
        nodes.pop();
    }
    cout << endl;
}

第二種解法:遞歸棧解

void printListReverse_2(ListNode *pHead) {
    ListNode *pNode = pHead;
    if (pNode->m_pNext != NULL) {
        printListReverse_2(pNode->m_pNext);
    }
    cout << pNode->m_Key << " ";
}

調用:

int main() {
    ListNode *head;
    add2Tail(&head, 0);
    add2Tail(&head, 1);
    add2Tail(&head, 2);
    add2Tail(&head, 3);
    printListReverse_1(head);
    printListReverse_2(head);
    return 0;
}

解:Java

對於解法來講,需要一個棧,但恍惚覺得棧類似的API在Java中應該並不存在。真的是這樣嗎?

Google了一下,居然真有這麼個類:Stack。它是Vector的子類。好吧,那就用起來唄。

import java.util.Stack;

public class Main {
    static class LinkNode {
        int mValue;
        LinkNode mNext;
    }

    static class SingleLink {
        public static LinkNode mHead;

        public void add2List(int value) {
            LinkNode newNode = new LinkNode();
            newNode.mValue = value;
            newNode.mNext = null;

            if (mHead == null) {
                mHead = newNode;
            } else {
                LinkNode node = mHead;
                while (node.mNext != null) {
                    node = node.mNext;
                }
                node.mNext = newNode;
            }
        }

        public void printReverse_1() { // 顯示棧解法
            Stack<LinkNode> stack = new Stack<>();
            LinkNode node = mHead;
            while (node != null) {
                stack.push(node);
                node = node.mNext;
            }

            while (!stack.empty()) {
                node = stack.pop();
                System.out.print(node.mValue + " ");
            }
            System.out.println();
        }

        public void printReverse_2(LinkNode node) { // 遞歸解法:這裏需要傳表頭節點
            if (node.mNext != null) {
                printReverse_2(node.mNext);
            }
            System.out.print(node.mValue + " ");
        }
    }

    public static void main(String args[]) {
        SingleLink singleLink = new SingleLink();
        singleLink.add2List(0);
        singleLink.add2List(1);
        singleLink.add2List(2);
        singleLink.add2List(3);
        singleLink.printReverse_1();
        singleLink.printReverse_2(singleLink.mHead);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章