上篇文章是數據結構的基礎部分,主要介紹了一些注意事項。node
今天開始線性表部分的梳理,線性表主要分爲了基礎概念和基本操做兩大部分,因爲某些過程或概念比較抽象,我添加了部分圖示,但願可以把這些抽象的東西直觀的表達出來。面試
基本操做模塊重點主要在單鏈表和順序表兩部分,本文着重梳理了線性表插入、刪除、查詢等基礎方法並搭配了部分實例供參考。算法
對於線性表來講,它是一組相同元素的有限序列,元素的個數就是線性表的長度,當元素個數爲 0 時,線性表就是空表。數組
數據結構包括邏輯結構、存儲結構和算法。線性表的基本概念這裏主要看線性表的邏輯結構和存儲結構就能夠了。數據結構
線性表的邏輯特性很好理解,因爲是相同元素的有限序列,能夠類比生活中的排隊場景:函數
只有一個表頭元素,表頭元素沒有前驅大數據
只有一個表尾元素,表尾元素沒有後繼spa
除表頭表尾元素外,其餘元素都只有一個前驅和一個後繼設計
線性表的存儲結構有兩類:順序表和鏈表。3d
特性:佔用一塊連續的存儲空間,隨機讀取,插入(刪除)時須要移動多個元素
鏈表
鏈表包含指針域與數值域兩部分,所以存儲不須要佔用連續空間,由指針來鏈接記錄結點位置信息,經過前驅節點的指針找到後繼結點。
特性:動態分配空間,順序讀取,插入(刪除)時不須要移動元素。
鏈表的分類以下:
單鏈表
每一個節點包含數據域與指針域,單鏈表分爲帶頭節點的和不帶頭結點的。
帶頭結點的鏈表中,頭結點的值域不含任何儲存數據的信息,從頭結點的下一個結點開始存儲數據信息,頭結點的指針 head 始終不等於 NULL,當 head -> next 等於 NULL 時,此時鏈表爲空
不帶頭結點的鏈表中,頭指針直接指向第一個結點,第一個結點就開始存儲數據信息,當 head 等於 NULL 時鏈表爲空。
注意區分頭結點和頭指針:
頭指針: 指向鏈表的第一個結點,不管帶不帶頭結點都有頭指針
頭結點:只有帶頭結點的鏈表纔有,值域只存描述鏈表屬性的信息,此時頭指針指向頭結點始終不爲 NULL 。
雙鏈表
雙鏈表在單鏈表的基礎上添加一個指針域指向前驅結點,能夠經過不一樣的指針域找到其前驅結點或後繼節點。
帶頭結點的雙鏈表,相似單鏈表,當 head -> next 爲空鏈表爲空
不帶頭結點的雙鏈表 當 head 爲空時鏈表爲空
循環單鏈表
在單鏈表的基礎上,將最後一個結點的指針域指向表頭結點便可。
帶頭結點的循環單鏈表,當 head 等於 head -> next 時 鏈表爲空
不帶頭結點的循環單鏈表,當 head 爲 空時鏈表爲空
循環雙鏈表
在雙鏈表的基礎上,將最後一個結點的尾指針指向第一個結點,將第一個結點的頭指針指向最後一個結點。
不帶頭結點的循環雙鏈表 當 head 爲空時 鏈表爲空
帶頭結點循環雙鏈表 當 head -> next (尾指針) 和 head -> prior (頭指針) 任一一個等於 head 時 ,鏈表爲空,事實上知足如下任一條件,鏈表都爲空:
head -> next = head head ->prior = head head -> next = head && head ->prior = head head -> next = head || head ->prior = head
靜態鏈表
靜態鏈表與通常鏈表不一樣,它通常來自於數組,數組中每一個節點包含兩個份量,一個是數據元素,一個是指針份量。
鏈表分類能夠理解成公路的分類,單鏈表像單行道,只能由表頭走向表尾;雙鏈表像雙行道能夠從表頭走向表尾,也能夠反過來;循環單鏈表像環形道,表頭表尾連接在一塊兒;循環雙鏈表像環形立交橋,表頭表尾鏈接在一塊兒,並且正向反向均可以。
順序表和鏈表的比較也算是面試中的經典題目了,這裏主要分爲時間角度和空間角度進行對比:
時間角度-存取方式的區別
順序表支持隨機讀取(查詢快),時間複雜度爲 O(1);
鏈表只能順序讀取(查詢慢),時間複雜度爲 O(n)
時間角度-插入(刪除)時須要移動元素的個數區別
順序表須要平均須要移動近通常的元素,時間複雜度爲 O(n),增刪慢
鏈表不須要移動元素,時間複雜度爲 O(1),增刪快
空間角度-存儲分配方式的區別
順序表內存一次性分配完,佔用連續存儲空間
鏈表存儲空間須要屢次分配,動態分配,來一個分配一個
空間角度-存儲密度區別 (存儲密度=結點值域所佔存儲量/結點結構所佔存儲量)
操做模塊主要爲單鏈表和順序表兩部分,着重梳理它們插入、刪除、查詢等基礎方法。
順序表定義
#define maxSize 100; struct typedef { int data [maxSize];//定義順序表存放元素的數據 int length; //定義順序表的長度 }Sqlist; // 順序表類型定義
單鏈表定義
struct typedef ListNode { int data, // 值域 struct ListNode *next;//指針域 }ListNode; //定義鏈表的結點類型
雙鏈表定義
struct typedef DLNode { int data; //值域 struct DLNode *prior;//前驅結點指針 struct DLNode *next;//後繼結點指針 }DLNode;//定義雙鏈表結點類型
操做部分就要結合例題來看了,順序表部分的操做相似 Java 中 數組的操做十分相似。
例1:已知一個順序表 L,其中元素遞增有序,設計一個算法,插入一個元素 m (int 型),後保持該順序表仍然遞增有序排列。(假設每次插入都是成功的)
分析題目能夠看出兩點:
1 原順序表 L 已經排序,遞增有序 2 插入 m 元素後仍然遞增有序,遞增排序不變 須要進行的步驟以下: 1 找出插入元素的位置 2 移動位置後面的元素 (從大下標的開始移動) 3 插入元素
代碼:
/** * 查找元素的方法 * l 順序表 * m 須要查找的元素 */ int findElement(SqList l,int m) { int i; for(i=0;i<l.length;++i) { if(m < l.data[i]) { return i; // 找到第一個比 m 大的元素的位置返回 } } return i;//若是整個順序表都不大於m,則返回最後的位置 } /** * 新增元素的方法 * l 順序表 * m 須要新增的元素 */ void insertElement(SqList &l,int m) // 順序表自己須要發生變化因此傳入的是引用型 { int p,i; p = findElement(l,m); for(i=l.length-1;i>=p;--i) // 條件爲 i>=p ,p位置的元素也須要移動 { l.data[i+1] = l.data[i];//從順序表的最後開始向右移動 } l.data[p] = m; ++(l.length); }
刪除操做與插入操做相反,刪除掉元素後,將後續元素都前移便可。
例2:刪除順序表L中下標爲 p (0<=p<=l.length-1)的元素,成功返回 1,不然返回0,並將刪除的數值賦值給 e。
分析題目可知:
1 須要刪除的元素位置爲 p
2 刪除元素前須要將值賦值給 e
須要進行的步驟以下:
1 找到須要刪除的元素的位置,題目已提供 p (若是沒有提供位置,須要循環查找)
2 將刪除元素 p 賦值給元素 e
3 將P後的元素左移 (與插入不一樣,刪除要從小下標的開始移動)
代碼:
/** * 刪除元素的方法 * l 順序表 * p 須要刪除元素的位置 * e 刪除元素賦值的變量 */ int deleteElement(SqList &l,int p,int &e)//須要改變的元素用引用變量 { int i; if( p < 0 || p > l.length -1) return 0; e = l.data[p]; for(i=p;i < l.length-1;++i){//判斷條件應爲 i < l.length-1 ,若是爲 i < l.length i+1 會下標越界 l.data[i] = l.data[i+1]; } --(l.length) return 1; }
鏈表的相關操做是數據結構中比較經常使用的,這部分須要劃重點。
單鏈表的插入主要有尾插法、頭插法兩種。
尾插法比較常規就是將新加的結點依次連接到鏈表最後一個結點。
尾插法: /** * C 準備要插入的鏈表 * a 數組,要插入到鏈表中的元素 * n 將要插入的節點數 * * *&C 指針型變量在函數體中須要改變的寫法 * 順序表 &L ( 普通變量 &m )引用型變量須要改變的寫法 * */ void createListR(ListNode *&C,int a[],int n) // 要改變的變量傳引用型 { ListNode *s,*r; // 指針r 準備指向 C,s準備指向要插入的節點 int i; // 循環使用的變量 C = (ListNode*) malloc (sizeof(ListNode)); //申請 C 的頭結點空間 C -> next = NULL; // 申請頭結點空間時必定不要忘記將頭結點指針指向NULL r = C; //r 指向頭節點 for(i=0;i<n,++i) { s = (ListNode*)malloc(sizeof(ListNode));//s 指向新申請的節點 s -> data = a[i]; // 值域賦值 r->next = s; // 插入新的結點 r = r->next;// 指針移動到終端結點,準備在終端插入新結點 } r ->next = NULL;//插入完成後將 ,終端結點的指針域設置爲NULL,C 創建完成 }
頭插法則是將新加的結點始終插入在頭結點的後面,所以越早插入的結點在鏈表中的位置實際上越靠後。
圖示:
頭插法: /** * C 準備要插入的鏈表 * a 數組,要插入到鏈表中的元素 * n 將要插入的節點數 * * *&C 指針型變量在函數體中須要改變的寫法 * 順序表 &L ( 普通變量 &m )引用型變量須要改變的寫法 * */ void createlistF(ListNode *&C,int a[],int n) { ListNode *s; int i ; C = (ListNode *)malloc( sizeof(ListNode)); C -> next = NULL; for(i=0;i<n;++i) { s = (ListNode*)malloc(sizeof(ListNode)); s->data = a[i]; //頭插法 s->next = C->next;//圖中第二步 C->next = s;//圖中第三步 } }
鏈表的刪除操做就比較簡單了,要刪除第m個結點,須要找到第 m-1 個結點,將第 m-1個結點的指針指向 m+1 個結點就能夠了。
相關操做:
q = p->next;//先將要刪除的結點賦值給q
p->next = p->next->next; //第二步操做
free(q);
例 3: 查找鏈表 L(帶頭結點) 中是否有一個值爲 m 的節點,若是有則刪除該節點,返回1,不然返回0.
/** * L 查找的鏈表 * m 鏈表值域查找的值 */ int deleteElement(ListNode *L,int m ) { ListNode *p,*q; // 定義一個指針 p,在鏈表中一直往下找 , q做爲刪除節點的 p = L; while(p->next != NULL) { if(p->next->data == x){ // 注意此處是 p->next->data ==x,而不是 p->next == x break; } p = p -> next; } if(p -> next == NULL) { return 0; } else { q = p->next; // 要刪除的節點是 p->next ,q p->next = p->next->next; free(q); return 1; } }
鏈表的基本的查詢 、插入、 刪除操做的重點部分已經回顧完了,下面來看看 leetCode 的例題---合併鏈表:
題目以下:
將兩個升序鏈表合併爲一個新的 升序 鏈表並返回。新鏈表是經過拼接給定的兩個鏈表的全部節點組成的。
示例:
輸入:1->2->4, 1->3->4 輸出:1->1->2->3->4->4
思路:
1 升序的兩個鏈表,合併成一個升序新鏈表
2 建立頭指針,使用尾插法循環比較 兩個鏈表的值,把值小的插入到頭結點後,移動指針
3 若是循環結束後某一個鏈表指針沒有移動到末尾,將新鏈表末尾指向這個指針的結點
圖解:
題解:
常規解法: /** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){ struct ListNode *head = (struct ListNode*)malloc(sizeof(struct ListNode));//申請頭結點空間 struct ListNode *r = head;//定義移動指針 r ,r始終指向終端結點 while( l1 !=NULL && l2 != NULL){ if(l1 -> val <= l2 -> val){ r -> next = l1;//將 r->next指向 l1 l1 = l1->next; //l1 指針前移 r = r->next; //r 指針前移 }else{ r -> next = l2; l2=l2 -> next; r = r-> next; } } r->next = NULL; if(l1 != NULL){ // 若是循環插入結束後仍有剩餘結點,直接插入到末尾 r -> next = l1; } if(l2 != NULL){// 若是循環插入結束後仍有剩餘結點,直接插入到末尾 r -> next = l2; } return head ->next;//不用返回頭結點 }
上面的解法結果沒什麼問題,就是咱們新建立了一個頭結點,若是置之不理的話,可能會致使內存泄漏。
下面是不建立頭結點的解法,只是再開始的時候巧妙的使用兩個鏈表中最小表頭爲新鏈表的頭結點,後面操做相似
不申請頭結點解法: /** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){ if(l1 == NULL) return l2; if(l2 == NULL) return l1; struct ListNode *head;//定義頭指針 if (l1->val < l2->val){ head = l1; //若是 l1 表頭元素值較小 ,將頭指針指向l1 l1 = l1->next;// l1 指針右移 }else{ head = l2; //若是 l2 表頭元素值較小 ,將頭指針指向l1 l2 = l2->next;//l2 指針右移 } struct ListNode *r = head; // l1,l2一直向後遍歷元素,向head中按序插入,直至l1或l2爲NULL while(l1 && l2){ if(l1->val < l2->val){ r->next = l1; l1 = l1->next; r = r->next; }else{ r->next = l2; l2 = l2->next; r = r->next; } } // l1或l2爲NULL,此時將不會空的鏈表接到最後便可 r->next = l1 ? l1 : l2; return head; }
以上不一樣的解法都是使用了鏈表的尾插法,由於尾插法正好符合題目的要求,新插入的結點也是依次遞增的。
若是題目要求變成要求 將兩個升序鏈表合併爲一個新的 降序 鏈表並返回,這時使用頭插法就比較合適了。
合併爲一個新的 降序 鏈表,頭插法: /** * Definition for singly-linked list. * struct ListNode { * int val; * struct ListNode *next; * }; */ struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){ struct ListNode *head = (struct ListNode*)malloc(sizeof(struct ListNode));//申請頭結點空間 head ->next =NULL; struct ListNode *r;//定義移動指針 r ,r始終指向終端結點 while( l1 !=NULL && l2 != NULL){ if(l1 -> val <= l2 -> val){ r = l1; // r 指針指向 l1 結點 l1 = l1->next;//l1 結點右移 r->next = head -> next ;//r->next 指向頭結點的下一個結點,見頭插法圖 head ->next = r; // 將 r 賦值給頭結點的下一個結點 }else{ r = l2; l2 = l2->next; r->next = head->next; head->next = r; } } while(l1){ // 若是循環插入結束後仍有剩餘結點,循環插入到頭結點後 r = l1; l1 = l1->next; r->next = head -> next ; head ->next = r; } while(l2){// 若是循環插入結束後仍有剩餘結點,循環插入到頭結點後 r = l2; l2 = l2->next; r->next = head->next; head->next = r; } return head ->next;//不用返回頭結點 }
以上就是本文的全部內容了,最後的例題只是拋磚引玉,單鏈表的好多複雜的操做,有興趣的能夠去找題刷刷~
最後,順序表和單鏈表的操做仍是比較重要的,後續雙鏈表、循環鏈表的操做基本都是在單鏈表的基礎上演變而來的,搞懂以上基礎部分,其餘的演變天然也就迎刃而解了。
PS: 關注「大數據江湖」公衆號, 後臺回覆 "鏈表合併",查看更多精彩內容。
— THE END —