鏈表
和數組不同,鏈表是一種動態的數據結構,在創建時並不需要知道他的長度。鏈表的結構很簡單,它通過指針(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個節點,只能從頭結點開始,沿指針往下遍歷鏈表,時間效率爲,而數組的時間效率爲。下面是查找到第一個含有某值的節點,並刪除的代碼。
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中以鏈表爲底層結構的是集合框架中的LinkedList
。LinkedList
中的鏈表爲雙向鏈表,來看看它的鏈表節點實現:
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);
}
}