在筆試面試考數據結構時,由於時間有限,所出的題不會是紅黑樹、平衡二叉樹等比較復雜的數據結構。鏈表結構簡單,題目規模小但需要仔細考慮細節,因此稱為筆試面試中的高頻考點。因此,下面總結出鏈表相關題目,以供復習。
1.比較順序表和鏈表的優缺點,說說他們分別在什么場景下使用?
2.從尾到頭打印單鏈表(劍指offer第五題)
3.刪除一個無頭單鏈表的非尾節點
4.在無頭單鏈表的一個非頭結點前插入一個節點
5.單鏈表實現約瑟夫環
6.逆置/反轉單鏈表
7.單鏈表排序(冒泡排序&快速排序)
8.合並兩個有序鏈表合並后依然有序
9.查找鏈表的中間節點,要求只能遍歷一次鏈表
10.查找單鏈表倒數第K個節點,要求只能遍歷一次鏈表
11.判斷單鏈表是否帶環?若帶環,求環的長度?求環的入口點?並計算每個算法的時間復雜度&空間復雜度。
12.判斷兩個鏈表是否相交,若相交,求交點(假設鏈表不帶環)
13.判斷兩個鏈表是否相交,若相交,求交點(假設鏈表帶環)
14.復雜鏈表的復制,一個鏈表的每個節點,有一個指向next指針指向下一個節點,還有一個random指針指向這個鏈表中的一個隨機節點或者NULL,現在要求實現復制這個鏈表
15.求兩個已排序鏈表中相同的數據。void UnionSet(ListNode* l1,ListNode* l2);
/////////////////////////////////////////////////////////////////////////////////////////////////////////(分割線)///////////////////////////////////////////////////
1.比較順序表和鏈表的優缺點,說說他們分別在什么場景下使用?
首先我們從順序表和鏈表的結構上來進行分析:
(1)對於順序表,無論是動態的還是靜態的,他們都是連續的存儲空間,在讀取上時間效率比較高,可通過地址之間的運算來訪問,但是在插入和刪除時會出現比較麻煩的負載操作。
(2)對於順序表,因為是鏈式存儲。因此在我們需要的時候我們才在堆上為他們開辟空間,鏈表對於插入刪除比較簡單,但是遍歷的話需要多次跳轉。
其次,我們從順序表和鏈表的空間申請方式來看:
(1)對於順序表,空間開辟是在順序表已滿的時候開辟,開辟次數較多的時候會出現較大的空間浪費
(2)對於鏈表,空間是針對單個節點的,不存在多余的空間浪費。並且在碎片內存池的機制下,可以有效的利用空間。
綜上所述:順序表一般用於查找遍歷操作比較頻繁的情況下使用,鏈表則針對於數據刪除修改操作比較多的情況下使用。
2.從尾到頭打印單鏈表
從尾到頭打印單鏈表有兩種解法,一種是利用棧把節點從頭到尾push進去,利用棧先進后出的特點,從尾到頭打印單鏈表節點,一種是利用遞歸,在輸出現有節點之前輸出下一個節點,循環直至最后一個節點,然后再將節點從尾到頭依次打印。
code1:利用棧
void PrintTailToHead(ListNode* head)
{
stack<int> st;
ListNode* p = head;
while (p != NULL)
{
st.push(p->_data);
p = p->_next;
}
while (!st.empty())
{
printf("%d->", st.top());
st.pop();
}
}
code2:利用遞歸
void PrintTailToHead(ListNode* head)
{
if (head != NULL)
{
while (head->_next != NULL)
{
PrintTailToHead(head->_next);
}
}
printf("%d->", head->data);
}
3.刪除一個無頭單鏈表的非尾節點
由於鏈表無頭,所以用常規方法刪除節點是不可能的。所以我們可以換種思路,將要刪除的節點后面的節點的值賦給要刪的節點,然后再把要刪除節點的后面的節點刪除,等於通過轉換,為被刪除節點創造了一個頭結點。代碼如下:
void DeleteNotTailNode(ListNode* p)
{
ListNode* s = p->_next;
assert(s);
p->_data = s->_data;
p->_next = s->_next;
free(p);
}
4.在無頭單鏈表的一個非頭結點前插入一個節點
這個題目跟上一個題目很像。在這個非頭結點后面插入一個節點,把這個非頭節點的值賦給新插入的節點,然后再把要插入的值賦給這個非頭節點即可。
void InsertNotHeadNode(ListNode* p, int data)
{
ListNode* s = (ListNode)malloc(sizeof(&ListNode));
assert(s);
s->_next =p->_next;
p->_next = s;
s->_data = p->_data;
p->_data = data;
}
5.單鏈表實現約瑟夫環(劍指offer第45題)
6.逆置/反轉單鏈表(劍指offer第16題)
7.單鏈表排序(冒泡排序&快速排序)
8.合並兩個有序鏈表合並后依然有序(劍指offer第17題)
這個題比較簡單,分別用指針指向兩個鏈表,比較兩個鏈表指針所指向節點的值,然后將節點取下來重新組成一個鏈表即可,代碼如下:
ListNode Merge(ListNode* head1, ListNode* head2)
{
if (head1 == NULL)
return head2;
if (head2 == NULL)
return head1;
ListNode* newhead = NULL;
if (head1->_data < head2->_data)
{
newhead = head1;
newhead->_next=Merge(head1->_next, head2);
}
if (head1->_data>head2->data)
{
newhead = head2;
newhead->_next = Merge(head1, head2->_next);
}
}
9.查找鏈表的中間節點,要求只能遍歷一次鏈表
查找鏈表的中間節點,但只能遍歷一次鏈表,所以我們會想到用快慢指針來解決這個問題。定義一個快指針,每次走兩步,載定義一個慢指針,每次走一步。等到快指針走到鏈表尾,慢指針所指向的節點就是鏈表的中間節點。代碼如下:
ListNode* FindMidNode(ListNode* head)
{
ListNode* fast;
ListNode* slow;
fast = head;
slow = head;
while (fast&&fast->_next)
{
slow = slow->_next;
fast = fase->_next->_next;
}
retrun slow;
}
10.查找單鏈表倒數第K個節點,要求只能遍歷一次鏈表(劍指offer第15題)
其實這個題跟上面的題很像,稍微轉化一下就能想出思路。我們可以定義兩個指針,一個指針先走K步,然后兩個指針同時移動,等到先走的指針走到鏈表尾部,后走的指針所指向的節點就是倒數第K個節點。要注意考慮鏈表的各種情況。代碼如下:
ListNode* FindKthNode(ListNode* head,int k)
{
if (head == NULL || k == 0)
return NULL;
ListNode* fast;
ListNode* slow;
fast = head;
slow = head;
for (int i = 0; i < k - 1; ++i) //要注意鏈表長度比K短的情況
{
if (fast->_next != NULL)
fast = fast->_next;
else retrun NULL;
}
while (fast->_next != NULL)
{
fast = fast->_next;
slow = slow->_next;
}
return slow;
}
11.判斷單鏈表是否帶環?若帶環,求環的長度?求環的入口點?並計算每個算法的時間復雜度&空間復雜度。(劍指offer第56題)
12.判斷兩個鏈表是否相交,若相交,求交點(假設鏈表不帶環)
13.判斷兩個鏈表是否相交,若相交,求交點(假設鏈表帶環)
14.復雜鏈表的復制,一個鏈表的每個節點,有一個指向next指針指向下一個節點,還有一個random指針指向這個鏈表中的一個隨機節點或者NULL,現在要求實現復制這個鏈表(劍指offer第26題)
15.求兩個已排序鏈表中相同的數據。void UnionSet(ListNode* l1,ListNode* l2);
16.在已排序的鏈表中刪除鏈表中重復的結點(劍指offer第57題)
