鏈表的基礎操做咱們都很熟悉,與此同時更流暢,完整,正確的解決,也是必不可少的基本功。所以我對它們作了一個總結,千萬不要小看這些基礎操做,熟悉它們之後你解決鏈表的問題將會駕輕就熟,那麼,開始吧!node
先給出鏈表原型,後續的全部過程都基於這個結構體。算法
struct ListNode {
int val;
struct ListNode *next;
};
複製代碼
四種基本操做:安全
建立,插入,刪除,反轉less
struct ListNode* createList(int n){
struct ListNode* head = NULL;
for (int i = 0; i < n; ++i)
{
struct ListNode* p = (struct ListNode*)malloc(sizeof(struct ListNode));
//Q1
if(!p)
return NULL;
//Q2
p->val = i;
p->next = head;
head = p;
}
return head;
}
複製代碼
要生成一個長度爲n的鏈表,每一個循環裏,new
一個p
節點,讓它的下一個節點指向鏈表頭,迭代,將這個p
節點更新爲鏈表頭,返回這個鏈表頭。函數
頭插法的特色在於你插的最後一個節點是鏈表頭,意思是你順序訪問鏈表時將獲得一個和插入序列相反的序列。測試
Q1:爲何檢查p?這樣操做有風險嗎?ui
由於動態內存分配存在失敗的可能,當失敗後返回的p是一個空指針,那麼接下來兩行對p的間接訪問都將報錯。this
即便這樣處理,依然存在一個問題,那就是咱們是在某一次循環過程當中malloc失敗,咱們讓函數返回,意味着整個createList函數失敗,而且咱們在前面成功分配的那部份內存並無回收,咱們甚至都沒有一個手段訪問到那些已經成功分配了的內存,那麼這個函數就會像一個黑洞,每一次失敗的調用,都會產生一部分空間的浪費,內存泄漏就極有可能會發生。所以,更周全的作法也許是在分配失敗時,咱們在函數內部free掉以前已經的內存。spa
不過好在這種狀況,在咱們寫到的函數裏基本不會發生,咱們能夠放心使用上面的作法生成一個鏈表。指針
Q2:有其餘的賦值方法嗎?最好方便測試?
固然,咱們能夠根據咱們對鏈表的操做對它進行修改。 例如咱們想對測試鏈表排序,咱們可讓i
換成rand
。 咱們想要刪除鏈表重複元素,咱們可讓它等於i / 3
,這樣會產生連續三個相同值的節點。 咱們想產生1-2-3-4-5而不是5-4-3-2-1那可讓它變成n-i
。
struct ListNode* createList(int n){
struct ListNode* head = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* tail = head;
for (int i = 0; i < n; ++i)
{
struct ListNode* p = (struct ListNode*)malloc(sizeof(struct ListNode));
if(!p)
return NULL;
p->val = i;
p->next = NULL;
tail->next = p;
tail = p;
}
//Q1
return head->next;
}
複製代碼
與頭插法相反,尾插法將每個新的p節點看成是鏈表的尾節點,它們的next域通通初始化爲NULL,迭代的是tail,每一次插入後讓tail指向新的p,對照着頭插法,你很快會弄清楚尾插的原理。
Q1:爲何返回的是head->next?
由於咱們第一次循環時,tail實際上同時也是head,這時的這第一個p應該就是咱們所但願返回的鏈表頭,head->next即是這一個p,因此我返回head->next。
插入的關鍵在於找到待插入位置以前的那個節點,改變它的next域,讓它指向插入的新節點p。所以,特殊的位置每每發生在鏈表頭,它以前沒有節點了,是NULL。咱們須要對這種特殊狀況考慮。固然,若是一個鏈表有一個val域無心義的頭節點dummyHead,這個問題也就不存在了。下面的即是一個有頭節點的例子,注意,有頭節點的鏈表在訪問時都要先往前推動一個。
咱們來看LeetCode上關於基礎操做的一道題:707.Design Linked List.
題目描述過長我就不引用了,上面連接過去看吧。
個人解答:
MyLinkedList* myLinkedListCreate() {
MyLinkedList* p = (MyLinkedList*)malloc(sizeof(MyLinkedList));
p->val = -1;
p->next = NULL;
return p;
}
int myLinkedListGet(MyLinkedList* obj, int index) {
if(index < 0)
return -1;
if(!obj->next)
return -1;
MyLinkedList* p = obj->next;
for (int i = 0; i < index; ++i)
{
p = p->next;
if(!p)
return -1;
}
return p->val;
}
void myLinkedListAddAtHead(MyLinkedList* obj, int val) {
MyLinkedList* p = (MyLinkedList*)malloc(sizeof(MyLinkedList));
if(!p)
return;
p->val = val;
p->next = obj->next;
obj->next = p;
}
void myLinkedListAddAtTail(MyLinkedList* obj, int val) {
MyLinkedList* p = obj;
while(p->next){
p = p->next;
}
MyLinkedList* newp = (MyLinkedList*)malloc(sizeof(MyLinkedList));
if(!newp)
return;
newp->val = val;
newp->next = NULL;
p->next = newp;
}
void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) {
if(index < 0)
return;
MyLinkedList* p = obj;
for (int i = 0; i < index; ++i)
{
p = p->next;
if(!p)
return;
}
MyLinkedList* newp = (MyLinkedList*)malloc(sizeof(MyLinkedList));
if(!newp)
return;
newp->val = val;
newp->next = p->next;
p->next = newp;
}
void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) {
if (index < 0)
return;
MyLinkedList* tmp, *p = obj;
if(!obj->next)
return;
for (int i = 0; i < index; ++i)
{
//ensure the delete node is not null
if(!p->next->next)
return;
p = p->next;
}
//p is before the node tobe deletes
//normal
tmp = p->next;
p->next = p->next->next;
free(tmp);
}
void myLinkedListFree(MyLinkedList* obj) {
MyLinkedList* tmp;
while(obj){
tmp = obj;
obj = obj->next;
free(tmp);
}
}
void myLinkedListPrint(MyLinkedList* obj){
MyLinkedList* p = obj->next;
while(p){
printf("%d->", p->val);
p = p->next;
}
printf("NULL\n");
}
複製代碼
其實刪除或許徹底能夠和插入放在一塊兒,它的重點仍然是找到它的前一個節點,若是刪除的是第一個元素,那麼這是一種特殊狀況,它的前面沒有節點,應該單獨考慮。
那麼不把它們放在一塊兒的緣由是什麼呢?刪除涉及到空間的回收,也就是free
的正確使用,這將是一個很容易出錯的地方。關於free
, 我會在最後再提到它。
例子能夠參考上面代碼裏的myLinkedListDeleteAtIndex()
。
來作幾道題吧!
203.Remove Linkedlist Elements
Problems
Remove all elements from a linked list of integers that have value val.
Example: Input: 1->2->6->3->4->5->6, val = 6 Output: 1->2->3->4->5
第一種作法,定義一個顯示的pre指針,並保證它的指向始終指向刪除元素的前一個位置,那麼顯然,對第一個元素的刪除是一種特殊狀況。
struct ListNode* removeElements_2(struct ListNode* head, int val) {
struct ListNode* cur = head, *pre = NULL, *tmp;
while(cur != NULL){
if(cur->val == val){
if(pre != NULL)
pre->next = cur->next;
else
head = head->next;
tmp = cur;
cur = cur->next;
free(tmp);
} else{
pre = cur;
cur = cur->next;
}
}
return head;
}
複製代碼
對於這種作法,我在第7行對這種狀況進行了處理。那就是改變head的指向而不是改變pre->next,由於你沒法對一個NULL指針進行->
運算,由於他實際上包含了指針的間接訪問,pre->next = (*pre).next
,這一點你在使用->
它的時候應該很清楚纔是。
關於空間釋放,這裏要釋放的固然是cur節點,由於要刪除的就是它,那麼爲何不直接free(cur)
呢?free(cur)
以後,咱們沒法訪問cur->nex
t,由於它已經被釋放掉了,那咱們怎麼推動cur
呢?答案是設置一個臨時指針,先將以前的cur
節點記錄下來,咱們在訪問事後,釋放那個tmp
節點,就很是安全了。
另外一種作法能夠避免刪除頭元素的特殊狀況,它使用到了指針的指針和&
運算符,取地址運算符也算是C的一大特性了,同時,這個例子也是指針的指針的經典運用。
struct ListNode* removeElements(struct ListNode* head, int val) {
struct ListNode ** p = &head, * tmp;
while(*p){
if((*p)->val == val){
tmp = *p;
*p = (*p)->next;
free(tmp);
}
else
p = &((*p)->next);
}
return head;
}
複製代碼
該算法中用來推動的是*p
,咱們發如今刪除時咱們改變了*p
,這實際上操縱的就是當前元素,意思是咱們的前一個元素無需作出改變,依然指向它原先指向的節點,而咱們對這個節點直接作出改變,讓它變成它的下一個節點,再釋放掉它,刪除就完成了。那麼爲何它能夠避免刪除頭元素的特殊狀況呢?由於咱們根本就不用考慮前一個節點了。
要改變一個指針
p
的內容咱們經過*p
實現,那麼要改變一個指針p
,咱們須要使用*pp
,其中pp = &p
,pp
實際上就是一個指針的指針。
那麼像
p1 = p2
這樣又表明着什麼呢?爲了弄清楚上面的內容,再舉一個例子。
int a = 3, b = 2;
a = b;
b = 4;
複製代碼
如今a = ?顯然 a = 2。 只是把b當前的值賦給了a。p1 = p2
一樣也只是讓p2當前的值賦給了p2,p1和p2之間在這行語句後就沒什麼聯繫了。
再來一道, 83.Remove Duplicates from Sorted List II
Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numbers from the original list.
Example 1:
Input: 1->2->3->3->4->4->5 Output: 1->2->5
Example 2:
Input: 1->1->1->2->3 Output: 2->3
個人解答:
struct ListNode* deleteDuplicates(struct ListNode* head) {
if(!head || !head->next)
return head;
struct ListNode* fakeNode = (struct ListNode*)malloc(sizeof(struct ListNode));
fakeNode->next = head;
struct ListNode* pre = fakeNode, *tmp;
//pre是第一個uinique元素出現的前一個節點
while(head){
//釋放了這些節點的內存,推動了head,但沒有改變指向
while(head->next && head->val == head->next->val){
tmp = head;
head = head->next;
free(tmp);
}
//說明head是unique的,直接往前推動
if(pre->next == head){
pre = pre->next;
head = head->next;
}
//一次性改變pre->next,pre不推動
else{
pre->next = head->next;
free(head);
head = pre->next;
}
}
return fakeNode->next;
}
複製代碼
這一樣是一個增長頭節點避免特殊狀況的例子,fakeNode就是這個頭節點。
須要注意的是,我採起的策略裏並非每刪除一個元素就改變它前一個節點的指向,而是在推動到不等於當前連續的這些值時,一次性改變pre->next讓它直接指向下一個,也就是值開始不一樣的那個節點。
關於刪除,leetcode上還有這些題能夠參考:237,83,707,82。
關於反轉鏈表,我會很天然的想到頭插法,那麼的確這樣,你徹底能夠用頭插法的視角來看待下面這個例子。
Reverse a singly linked list.
Example:
Input: 1->2->3->4->5->NULL Output: 5->4->3->2->1->NULL
struct ListNode* reverseList(struct ListNode* head) {
struct ListNode* tmpHead = NULL;
struct ListNode* p = head;
while(p != NULL){
struct ListNode* tmp = p->next;
p->next = tmpHead;
tmpHead = p;
p = tmp;
}
return tmpHead;
}
複製代碼
很簡單,對不對,就是遍歷原鏈表,再將節點依次用頭插法的方式插入。值得注意的是,如今鏈表的結構已經改變,咱們作的是in-place
,也就是在原有空間上作出的修改,咱們如今的head
已經變成了什麼?
事實上,觀察一個鏈表的結構有沒有被破壞,咱們只用看對鏈表的訪問過程當中咱們有沒有對p->next
有沒有改變,即便p不是head自己,在本例中咱們第一次循環時便改變了它,這個時候head->next
變成了NULL,對,它變成了尾節點。改變p
對原鏈表的結構其實是沒有影響的。想一想爲何?
再看一例:
25. Reverse Nodes in k-Group
Given a linked list, reverse the nodes of a linked list k at a time and return its modified list. k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is.
Example: Given this linked list: 1->2->3->4->5 For k = 2, you should return: 2->1->4->3->5 For k = 3, you should return: 3->2->1->4->5
思路:一次選擇k個元素一組,這一組中第一個元素設爲first
,而後將這個組反轉,kHead
是這個反轉子串的頭,設立一個pre
指針,始終指向反轉後的最後一個元素,開始時pre
指向一個dummyHead,也就是這裏的head2
。每次循環結束前讓pre->next
指向新的kHead
。
個人解答:
struct ListNode* reverseKGroup(struct ListNode* head, int k) {
int length = 0;
struct ListNode* p = head;
while(p){
p = p->next;
length += 1;
}
if(length < k)
return head;
int res_node = length;
struct ListNode* head2 = (struct ListNode*)malloc(sizeof(struct ListNode));
struct ListNode* pre = head2;
struct ListNode* kHead = NULL, *tmp, *first = NULL;
while(res_node >= k){
int n = k;
kHead = NULL;
first = NULL;
while(n--){
first = first?first:head;
tmp = head->next;
head->next = kHead;
kHead = head;
head = tmp;
}
pre->next = kHead;
pre = first;
res_node -= k;
}
pre->next = head;
return head2->next;
}
複製代碼
dummyHead真香~
更多關於reverse的還有成對交換啊,指定位置反轉啊,利用反轉花式改變鏈表結構啊,利用反轉判斷迴文啊,leetcode上都有對應的題目,能夠按tag去搜索挑戰一下。