給定一個帶頭結點的單鏈表,編寫算法將其原地逆置。所謂“原地”是指空間復雜度為O(1)。有兩種方法,頭插法和冒泡法。這兩種方法的時間復雜度均為O(n)。
頭插法
思路
我們知道,用頭插法建立鏈表,得到的鏈表中元素的順序和輸入的順序相反,所以利用這一特點,可以將鏈表逆置。
給定一個帶頭結點的單鏈表L,如下圖所示。
首先用指針p存儲鏈表第一個結點,然后將頭結點從鏈表中剝離下來,如下圖所示,此時鏈表L只有一個頭結點。
另設一指針r保存p的后繼,將p指向的結點N1用尾插法插入到鏈表L中,
此時p指向N2,保存p的后繼N3,再將N2尾插到鏈表L中,
以此類推,直至保存后繼的指針r為空,退出循環。
頭插法的實現代碼
void Reverse_L1(Linklist L)
{
/* p 為工作指針, r 為 p 的后繼, 以防斷鏈. */
LNode *p, *r;
/* 從第一個元素開始. */
p = L->next;
/* 先將頭結點剝離出來. */
L->next = NULL;
/* 依次將元素摘下. */
while (p != NULL)
{
/* 暫存 p 的后繼. */
r = p->next;
/* 用頭插法插入 p. */
p->next = L->next;
L->next = p;
/* 更新 p, 指向下一個結點. */
p = r;
}
return;
}
冒泡法
我把這種方法稱為“冒泡法”,是因為算法流程和冒泡排序類似,只不過在冒泡排序中是相鄰的元素出現逆序才交換,而鏈表逆置則要求每對結點之間都要交換順序。
思路
冒泡法和頭插法不同,頭插法只有一個工作指針p指向一個操作對象——被摘下來的結點,以及存儲其后繼的指針r。而冒泡法有兩個工作指針,即一對工作指針,以及存儲其后繼的指針r,共計3個指針。考慮如下一般情況,
指針pre和p分別指向兩個結點,將其看作一對結點,它們是每次循環操作的對象。循環中,讓N2的后繼指向N1,即完成了(N1,N2)的逆置。之后三個指針進一,pre=p,p=r,r=r->next,重復上述逆置操作,鏈表變成了下圖。
顯而易見,如果指針r!=NULL
,則循環還要繼續下去,若r==NULL
,循環結束,鏈表逆置完成,而指針p指向逆置后的一個元素。
上述步驟發生的前提是鏈表中除了頭結點以外,至少有兩個結點,而為了將判斷是否繼續循環和是否進入循環結合起來,應將指針r初始置為指向第一個結點的后繼,p指向第一個結點,即p=L->next,r=p->next
,指針pre無強制要求。
注意初始p指向的結點在完成逆置之后將成為最后一個結點,所以應該在逆置之前將p的后繼置為空,否則逆置之后p的后繼會指向倒數第二個結點,而倒數第二個結點的后繼指向倒數第一個結點,即逆置前的p,兩個結點之間形成環。
冒泡法的實現代碼
void Reverse_L2(Linklist L)
{
/**
* 三個指針, p 為要反轉的結點, pre 為 p 前面的結點, r 是保存 p 后繼的指針.
* 初始狀態 p 指向第一個元素, r 指向第二個元素.
*/
LNode *pre = L, *p = L->next, *r = p->next;
/**
* 將要第一個結點后繼鏈接斷開. 因為它將成為逆置后鏈表的最后一個結點, 否則
* 將在逆置后的鏈表中的最后一個元素和倒數第二個元素之間形成環.
*/
p->next = NULL;
/**
* 如果 r 不為空, 三個指針前進一個單位.
* 否則鏈表只有一個結點, 直接執行 while 后的 L->next = p.
* 循環中將 p 反轉指向其前面的結點 pre, r 是 p 的后繼.
* 假設鏈表有 n 個元素, 則循環可將前 n - 1 個元素逆置,
* 之后 p 指向第 n 個元素.
*/
while (r != NULL)
{
/* 指針依次進一. */
pre = p;
p = r;
r = r->next;
/* 逆置. */
p->next = pre;
}
/* 頭結點指向逆置之后的第一個結點. */
L->next = p;
return;
}