上一篇簡單的開了一個頭,簡單介紹了一下所謂的時間複雜度與空間複雜度,從這篇開始將陸陸續續寫一下經常使用的數據結構:鏈表、隊列、棧、樹等等。算法
鏈表當初是我在學校時惟一死磕過的數據結構,那個時候本身還算是一個好學生,雖然上課沒怎麼聽懂,可是課後仍是根據仔細調試過老師給的代碼,硬是本身給弄懂了,它是我離校時惟一可以寫出實現的數據結構,如今回想起來應該是它比較簡單,算法也比較直來直去吧。雖然它比較簡單,不少朋友也都會鏈表。可是做爲一個系列,若是僅僅由於它比較簡單而不去理會,總以爲少了點什麼,因此再這仍然將其列舉出來。數組
單向鏈表是鏈表中的一種,它的特色是隻有一個指向下一個節點的指針域,對單向鏈表的訪問須要從頭部開始,根據指針域依次訪問下一個節點,單向鏈表的結構以下圖所示
數據結構
單向鏈表的結構只須要一個數據域與指針域,這個數據域能夠是一個結構體,也能夠是多個基本數據類型;指針域是一個指向節點類型的指針,簡單的定義以下:函數
typedef struct _LIST_NODE { int nVal; struct _LIST_NODE *pNext; }LIST_NODE, *LPLIST_NODE;
建立鏈表能夠採用頭插法或者尾插法來初始化一個有多個節點的鏈表oop
頭插法的示意圖以下:
指針
它的過程就像示意圖中展示的,首先使用新節點p的next指針指向當前的頭節點把新節點加入到鏈表頭,而後變動鏈表頭指針,這樣就在頭部插入了一個節點,用代碼來展現就是調試
p->next = head; head = p;
咱們使用一個函數來封裝就是code
LPLIST_NODE CreateListHead() { LPLIST_NODE pHead = NULL; while (TRUE) { LPLIST_NODE p = (LPLIST_NODE)malloc(sizeof(LIST_NODE)); if (NULL == p) { break; } memset(p, 0x00, sizeof(LIST_NODE)); printf("請輸入節點值(爲0時將退出建立節點):"); scanf_s("%d", &p->nVal); //這裏不須要對鏈表爲空單獨討論 //當鏈表爲空時pHead 的值爲NULL, 這兩句代碼就變爲 //p->pNext = NULL; //pHead = p; p->pNext = pHead; pHead = p; if (p->nVal == 0) { break; } } return pHead; }
採用尾插法的話,首先得得到鏈表的尾部 pTail, 而後使尾節點的next指針指向新節點,而後更新尾節點,用代碼來表示就是blog
pTail->next = p; pTail = p;
下面的函數是採用尾插法來構建鏈表的例子隊列
//這個函數多定義了一個變量用來保存 // 能夠不須要這個變量,這樣在插入以前須要遍歷一遍鏈表,以便找到尾節點 // 可是每次插入以前都須要遍歷一遍,沒有定義一個變量保存尾節點這種方式來的高效 LPLIST_NODE CreateListTail() { LPLIST_NODE pHead = NULL; LPLIST_NODE pTail = pHead; while (NULL != pTail && NULL != pTail->pNext) { pTail = pTail->pNext; } while (TRUE) { LPLIST_NODE p = (LPLIST_NODE)malloc(sizeof(LIST_NODE)); if (NULL == p) { break; } memset(p, 0x00, sizeof(LIST_NODE)); printf("請輸入節點值(爲0時將退出建立節點):"); scanf_s("%d", &p->nVal); //因爲這種方法須要對尾節點的next域賦值,因此須要考慮鏈表爲空的狀況 if (NULL == pTail) { pHead = p; pTail = pHead; }else { pTail->pNext = p; pTail = p; } if (p->nVal == 0) { break; } } return pHead; }
鏈表的每一個節點在內存中不是連續的,因此它不能像數組那樣根據下標來訪問(固然能夠利用C++中的運算符重載來實現使用下標訪問),鏈表中的每個節點都保存了下一個節點的地址,因此咱們根據每一個節點指向的下一個節點來依次訪問每一個節點,訪問的代碼以下:
void TraverseList(LPLIST_NODE pHead) { while (NULL != pHead) { printf("%d\n", pHead->nVal); pHead = pHead->pNext; } }
鏈表的每一個節點都是在堆上分配的,在再也不使用的時候須要手工清除每一個節點。清除時須要使用遍歷的方法,一個個的刪除,只是須要在遍歷的指針移動到下一個節點前保存當前節點,以便可以刪除當前節點,刪除的函數以下
void DestroyList(LPLIST_NODE pHead) { LPLIST_NODE pTmp = pHead; while (NULL != pTmp) { pTmp = pHead->pNext; delete pHead; pHead = pTmp; } }
如上圖所示,假設咱們要刪除q節點,那麼首先須要遍歷找到q的上一個節點p,將p的next指針指向q的下一個節點,也就是賦值爲q的next指針的值,用代碼表示就是
p->next = q->next;
刪除節點的函數以下:
void DeleteNode(LPLIST_NODE* ppHead, int nValue) { if (NULL == ppHead || NULL == *ppHead) { return; } LPLIST_NODE p, q; p = *ppHead; while (NULL != p) { if (nValue == p->nVal) { if (*ppHead == p) { *ppHead = p->pNext; free(p); }else { q->pNext = p->pNext; free(p); } p = NULL; q = NULL; break; } q = p; p = p->pNext; } }
在上述代碼中首先來遍歷鏈表,找到要刪除的節點p和它的上一個節點q,因爲頭節點沒有上一個節點,因此須要特別判斷一下須要刪除的是否爲頭節點,若是爲頭結點,則直接將頭指針指向它的下一個節點,而後刪除頭結點便可,若是不是則採用以前的方法來刪除。
如上圖所示,若是須要在q節點以後插入p節點的話,須要兩步,將q的next節點指向q,而後將q指向以前p的下一個節點,這個時候須要注意一下順序,若是咱們先執行q->next = p 的話,那麼以前q的下一個節點的地址就被覆蓋掉了,這個時候後面的節點都丟掉了,因此這裏咱們要先執行p->next = q->next 這條語句,而後在執行q->next = p
下面是一個建立有序鏈表的例子,這個例子演示了在任意位置插入節點
LPLIST_NODE CreateSortedList() { LPLIST_NODE pHead = NULL; while (TRUE) { LPLIST_NODE p = (LPLIST_NODE)malloc(sizeof(LIST_NODE)); if (NULL == p) { break; } memset(p, 0x00, sizeof(LIST_NODE)); printf("請輸入節點值(爲0時將退出建立節點):"); scanf_s("%d", &p->nVal); if (NULL == pHead) { pHead = p; }else { if (pHead->nVal > p->nVal) { p->pNext = pHead; pHead = p; }else { LPLIST_NODE q = pHead; LPLIST_NODE r = q; q = q->pNext; while (NULL != q && q->nVal < p->nVal) { r = q; q = q->pNext; } p->pNext = r->pNext; r->pNext = p; } } if (p->nVal == 0) { break; } } return pHead; }
當肯定新節點的值以後,首先遍歷鏈表,直到找到比新節點中數值大的節點,那麼這個新節點就是須要插入到該節點以前。在遍歷的時候使用r來保存以前的節點。這裏須要注意這些狀況:
可是在代碼中並無考慮到尾部插入的狀況,因爲在尾部插入時,r等於尾節點,r->pNext 的值爲NULL, 因此 p->pNext = r->pNext;r->pNext = p;
能夠當作 p->pNext = NULL; r->pNext = p;
也就是將p的next指針指向空,讓其做爲尾節點,將以前的尾節點的next指針指向新節點。
循環鏈表是創建在單向鏈表的基礎之上的,循環鏈表的尾節點並不指向空,而是指向其餘的節點,能夠是頭結點,能夠是自身,也能夠是鏈表中的其餘節點,爲了方便操做,通常將循環鏈表的尾節點的next指針指向頭節點,它的操做與單鏈表的操做相似,只須要將以前判斷尾節點的條件變爲 pTail->pNext == pHead
便可。這裏就再也不詳細分析每種操做了,直接給出代碼
LPLIST_NODE CreateLoopList() { LPLIST_NODE pHead = NULL; LPLIST_NODE pTail = pHead; while(1) { LPLIST_NODE p = (LPLIST_NODE)malloc(sizeof(LIST_NODE)); if (NULL == p) { break; } memset(p, 0x00, sizeof(LIST_NODE)); printf("請輸入一個值:"); scanf_s("%d", &p->nVal); if (NULL == pHead) { pHead = p; p->pNext = pHead; pTail = pHead; }else { pTail->pNext = p; p->pNext = pHead; pTail = p; } if (0 == p->nVal) { break; } } return pHead; } void TraverseLoopList(LPLIST_NODE pHead) { LPLIST_NODE pTmp = pHead; if (NULL == pTmp) { return; } do { printf("%d, ", pTmp->nVal); pTmp = pTmp->pNext; } while (pTmp != pHead); } void DestroyLoopList(LPLIST_NODE pHead) { LPLIST_NODE pTmp = pHead; LPLIST_NODE pDestroy = pTmp; if (NULL == pTmp) { return; } do { pTmp = pDestroy->pNext; free(pDestroy); pDestroy = pTmp; }while (pHead != pTmp); }
在上面說過,循環鏈表的尾指針不必定指向頭節點,它能夠指向任何節點,那麼該怎麼判斷一個節點是否爲循環鏈表呢?既然它能夠指向任意的節點,那麼確定是找不到尾節點的,並且堆內存的分配是隨機的,咱們也不可能按照指針變量的大小來判斷哪一個節點在前哪一個在後。
回想一下在學校跑一公里的時候是否是回出現這樣的狀況,跑的塊的會領先跑的慢的一週?根據這種情形咱們能夠考慮使用這樣一種辦法:定義兩個指針,一個一次走兩步也是就是p = p->next->next, 一個慢指針一次走一步,也就是q = q->next,若是是循環鏈表,那麼快指針在某個時候必定會領先慢指針一週,也就是達到 p == q 這個條件,不然就是非循環鏈表。根據這個思路,能夠考慮寫下以下代碼:
bool IsLoopList(LPLIST_NODE pHead) { if (NULL == pHead) { return false; } LPLIST_NODE p = pHead; LPLIST_NODE q = pHead->pNext; while (NULL != p && NULL != q && NULL != q->pNext && p != q) { p = p->pNext; q = q->pNext->pNext; } if (q == NULL || NULL == p || NULL == q->pNext) { return false; } return true; }
以前在插入或者刪除的時候,須要定義兩個指針變量,讓其中一個一直更在另外一個的後面,單向鏈表有一個很大的問題,不能很方便的找到它的上一個節點,爲了解決這一個問題,提出了雙向鏈表,雙向鏈表與單向相比,多了一個指針域,用來指向它的上一個節點,也就是以下圖所示:
雙向鏈表的操做與單向鏈表的相似,只是多了一個指向前一個節點的指針域,它要考慮的狀況與單向鏈表類似
刪除節點的示意圖以下:
假設刪除的節點p,那麼首先根據p的pre指針域,找到它的上一個節點q,採用與單向鏈表相似的操做:
q->next = p->next; p->next->pre = q;
下面是刪除節點的例子:
void DeleteDNode(LPDLIST_NODE* ppHead, int nValue) { if (NULL == ppHead || NULL == *ppHead) { return; } LPDLIST_NODE p = *ppHead; while (NULL != p && p->nVal != nValue) { p = p->pNext; } if (NULL == p) { return; } if (*ppHead == p) { *ppHead = (*ppHead)->pNext; p->pPre = NULL; free(p); } else if (p->pNext == NULL) { p->pPre->pNext = NULL; free(p); }else { p->pPre->pNext = p->pNext; p->pNext->pPre = p->pPre; } }
插入節點的示意圖以下:
假設新節點爲p,插入的位置爲q,則插入操做能夠進行以下操做
p->next = q->next; p->pre = q; q->next->pre = p; q->next = p;
也是同樣要考慮不能覆蓋q的next指針域不然可能存在找不到原來鏈表中q的下一個節點的狀況。因此這裏先對p的next指針域進行操做
下面也是採用建立有序列表的例子
LPDLIST_NODE CreateSortedDList() { LPDLIST_NODE pHead = NULL; while (1) { LPDLIST_NODE pNode = (LPDLIST_NODE)malloc(sizeof(DLIST_NODE)); if (NULL == pNode) { return pHead; } memset(pNode, 0x00, sizeof(DLIST_NODE)); printf("請輸入一個整數:"); scanf_s("%d", &pNode->nVal); if(NULL == pHead) { pHead = pNode; }else { LPDLIST_NODE q = pHead; LPDLIST_NODE r = q; while (NULL != q && q->nVal < pNode->nVal) { r = q; q = q->pNext; } if (q == pHead) { pNode->pNext = pHead; pHead->pPre = pNode; pHead = pNode; }else if (NULL == q) { r->pNext = pNode; pNode->pPre = r; }else { pNode->pPre = r; pNode->pNext = q; r->pNext = pNode; q->pPre = pNode; } } LPDLIST_NODE q = pHead; LPDLIST_NODE r = q; if (0 == pNode->nVal) { break; } } return pHead; }
鏈表還有一種是雙向循環鏈表,對於這種鏈表主要是在雙向鏈表的基礎上,將頭結點的pre指針指向某個節點,將尾節點的next節點指向某個節點,並且這兩個指針能夠指向同一個節點也能夠指向不一樣的節點,通常在使用中都是head的pre節點指向尾節點,而tail的next節點指向頭節點。這裏就再也不詳細說明,這些鏈表只要掌握其中一種,剩下的很好掌握的。