【圖解數據結構】 線性表

1.線性表的定義

若將線性表記爲(a1,...,ai-1,ai,ai+1,...,an),則表中ai-1領先於ai,ai領先於ai+1,稱ai-1是ai的直接前驅元素,ai+1是ai的直接後繼元素。
線性表元素的個數n(n>=0)定義爲線性表的長度,當n=0時,稱爲空表。
mark算法

2.線性表的順序存儲結構

線性表的順序存儲結構,指的是一段地址連續的存儲單元依次存儲線性表的數據元素。數組

線性表的順序存儲結構如圖所示:函數

mark

2.1地址計算方法

用數組存儲順序表意味着要分配固定長度的數組空間,分配的數組空間大於等於當前線性表的長度,數據元素的序號和存放它的數組下標之間存在對應關係:性能

mark

存儲器的每一個存儲單元都有本身的編號,這個編號稱爲地址。學習

每一個數據元素都須要佔用必定的存儲單元空間的,假設佔用的是c個存儲單元,對於第i個數據元素ai存儲位置爲(LOC表示得到存儲位置的函數):測試

LOC(ai) = LOC(a1) + (i-1)*c動畫

mark

2.2線性表順序存儲的結構代碼:

#define MAXSIZE 20 /*存儲空間初始分配量*/
typedef int ElemType;

typedef struct 
{
    ElemType data[MAXSIZE]; /*數組存儲數據元素*/
    int length;             /*線性表當前長度*/
}SqList;

描述線性表順序存儲的三個屬性:spa

  • 存儲空間的起始位置:數組data,它的位置就是存儲空間的存儲位置。
  • 線性表的最大存儲容量:數組長度MAXSIZE。
  • 線性表的當前長度:length。

2.3得到元素操做

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
/*用e返回L中第i個數據元素的值*/
Status GetElem(SqList L,int i, ElemType *e) {
    if (L.length = 0 || i<1 || i>L.length) {
        return ERROR;
    }
    *e = L.data[i - 1];
    return OK;
}

2.4插入操做

思路指針

  • 若是線性表長度大於等於數組長度,拋出異常code

  • 若是插入位置不合理,拋出異常

  • 從最後一個元素開始向前遍歷到第i個位置,分別將它們都向後移動一個位置

  • 將要插入元素填入位置i

  • 表長加1

/*在L中第i個位置以前插入新的數據元素e,L的長度加1*/
Status ListInsert(SqList *L, int i, ElemType e)
{
    int k;
    if (L->length == MAXSIZE)   /*順序線性表已滿*/
    {
        return ERROR;
    }
    if (i<1 || i>L->length + 1) /*i不在範圍內*/
    {
        return ERROR;
    }
    if (i <= L->length) /*插入位置不在表尾*/
    {
        for (k = L->length-1; k >=i-1 ; k--)
        {
            L->data[k + 1] = L->data[k];
        }
    }
    L->data[i - 1] = e;
    L->length++;
    return OK;
}

插入前

mark

插入後

mark

2.5刪除操做

思路

  • 若是爲空表,拋出異常
  • 若是刪除位置不合理,拋出異常
  • 從刪除元素位置開始遍歷到最後一個元素位置,分別將它們向前移動一個位置
  • 表長減1
/*刪除L的第i個數據元素,並用e返回其值,L的長度減1*/
Status ListDelete(SqList *L, int i, ElemType *e)
{
    int k;
    if (L->length == 0)
    {
        return ERROR;
    }
    if (i<1 || i>L->length)
    {
        return ERROR;
    }
    *e = L->data[i - 1];
    if (i < L->length)
    {
        for (k = i; k <= L->length; k++)
        {
            L->data[k - 1] = L->data[k];
        }
    }
    L->length--;
    return OK;
}

刪除前

mark

刪除後

mark

2.6優缺點

線性表的順序存儲結構,在存、讀數據時,不論是哪一個位置,時間複雜度都是O(1);而插入或刪除時,時間複雜度都是O(n)。

優勢:

  • 無需爲表示線性表中的邏輯關係而增長額外的存儲空間
  • 能夠快速的存取線性表中任一位置的元素

缺點:

  • 插入和刪除操做須要移動大量的元素

  • 難以肯定線性表存儲空間的容量

  • 形成存儲空間的「碎片」,浪費存儲空間

3.線性表的鏈式存儲結構

爲了每一個數據元素ai與其後繼數據元素ai+1之間的邏輯關係,對數據元素ai來講,除了存儲自己的信息以外,還須要存儲一個指示其後繼元素的信息(即直接後繼元素的存儲位置)。
mark

3.1單鏈表

n個結點鏈結成一個鏈表,每一個結點只包含一個指針域,叫作單鏈表。

線性鏈表中第一個結點的存儲位置叫作頭指針,整個鏈表的存取必須從頭指針開始。 線性鏈表的最後一個結點指針爲「空」(一般用NULL或^表示)。

單鏈表存儲示意圖:

mark

空鏈表:

mark

3.1.1線性錶鏈式存儲的結構代碼:

/*線性表的單鏈表存儲結構*/
typedef int ElemType;
typedef struct Node
{
    ElemType data;
    struct Node *next;
} Node;
typedef struct Node *LinkList;

3.1.2單鏈表的讀取

在單鏈表中讀取第i個元素,咱們沒法一開始知道,必須從頭開始找。

讀取單鏈表中第i個數據的思路:

  1. 聲明一指針p指向單鏈表第一個節點,初始化j=1
  2. 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷的指向下一節點,j累加1
  3. 若到鏈表末尾p爲空,則說明第i個節點不存在
  4. 不然查找成功,返回節點p的數據

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操做結果:用e返回L中第i個數據元素的值*/
Status GetElem(LinkList L, int i, ElemType *e)
{
    int j;
    LinkList p;
    p = L->next;    /*讓指針p指向鏈表L的第一個節點*/
    j = 1;
    while (p && j<i)    /*p不爲空且計數器j尚未等於i時,循環繼續*/
    {
        p = p->next;
        ++j;
    }
    if (!p || j > i)
    {
        return ERROR;    /*第i個節點不存在*/
    }
    *e = p->data;   /*取第i個節點的數據*/
    return OK;
}

動畫模擬:

mark

3.1.3單鏈表的插入

假設存儲元素e的節點爲s,只須要將節點s插入到節點p和p->next之間便可。

s->next = p->next;
p->next = s;

也就是說讓p的後繼節點改爲s的後繼節點,再把節點s變成p的後繼節點。

mark

注意:s->next = p->next;p->next = s;代碼的順序不能反。若是先p->next = s;,再s->next = p->next;,此時第一句會將p->next覆蓋成s的地址了,那麼s->next = p->next;實際上就等於s->next = s;。這樣單鏈表將再也不連續,插入操做就是失敗的。對於單鏈表的表頭和表尾的特殊狀況,操做是相同的。

單鏈表第i個數據插入節點的思路:

  1. 聲明一指針p指向單鏈表頭結點,初始化j=1
  2. 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷的指向下一節點,j累加1
  3. 若到鏈表末尾p爲空,則說明第i個節點不存在
  4. 不然查找成功,生成一個空節點s做爲插入節點
  5. 將數據元素e賦值給s->data
  6. 單鏈表插入的標準語句s->next = p->next;p->next = s;

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;
typedef struct Node
{
    ElemType data;
    struct Node *next;
} Node;
typedef struct Node *LinkList;

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操做結果:在L中第i個節點位置以前插入新的數據元素e,L的長度加1*/
Status ListInsert(LinkList *L, int i, ElemType e)
{
    int j;
    LinkList p = *L;
    j = 1;

    while (p && j<i)    /*尋找第i-1個節點*/
    {
        p = p->next;
        ++j;
    }
    if (!p || j > i)
    {
        return ERROR;   /*第i個節點不存在*/
    }
    LinkList s = (LinkList)malloc(sizeof(Node));    /*生成新節點*/

    s->data = e;
    s->next = p->next; /*將p的後集節點賦值給s的後繼*/
    p->next = s ;   /*將s賦值給p的後繼*/
    return OK;
}

c語言的malloc標準函數,用於生成一個新的節點,實質就是在內存中分配內存用來存放節點。

測試代碼:

int main()
{
    LinkList head = (LinkList)malloc(sizeof(Node)); /*頭結點*/

    LinkList s1 = (LinkList)malloc(sizeof(Node));   /*第一個節點*/
    s1->data = 4;
    s1->next = NULL;

    head->next = s1;
    ListInsert(&head, 1, 2);     /*第1個節點前插入2*/
    ListInsert(&head, 2, 3);    /*第2個節點前插入3*/
    ListInsert(&head, 2, 7);    /*第1個節點前插入7*/
    ListInsert(&head, 3, 5);    /*第1個節點前插入5*/
}

運行結果:

mark

動畫模擬:

mark

3.1.4單鏈表的刪除

假設存儲元素ai的節點爲q,要實現從單鏈表中將節點q刪除的操做,實際上是將它的前繼節點的指針指向它的後繼節點便可。

mark

q = p->next;
p->next = q->next;

單鏈表第i個數據刪除節點的算法:

  1. 聲明一指針p指向單鏈表頭結點,初始化j=1
  2. 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷的指向下一節點,j累加1
  3. 若到鏈表末尾p爲空,則說明第i個節點不存在
  4. 不然查找成功,將欲刪除的節點p->next賦值給q
  5. 將q節點中的數據賦值給e,做爲返回
  6. 釋放q節點

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;
typedef struct Node
{
    ElemType data;
    struct Node *next;
} Node;
typedef struct Node *LinkList;

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操做結果:刪除L中第i個節點,並用e返回其值,L的長度減1*/
Status ListDelete(LinkList *L, int i, ElemType *e)
{
    int j;
    LinkList p = *L;
    j = 1;
    while (p->next && j<i)  /*尋找第i-1個節點*/
    {
        p = p->next;
        ++j;
    }
    if (!(p->next) || j > i)
    {
        return ERROR;       /*第i個節點不存在*/
    }
    LinkList q = p->next;
    p->next = q->next;      /*將q的後繼賦值給p的後繼*/
    *e = q->data;   /*將q節點中的數據給e*/

    free(q);        /*回收此節點,釋放內存*/

    return OK;
}

c語言的free標準函數,做用是讓系統回收一個節點,釋放內存。

測試代碼:

仍是使用上面插入例子的單鏈表,而後刪除單鏈表中的第3個節點:

int main()
{
    LinkList head = (LinkList)malloc(sizeof(Node)); /*頭結點*/

    LinkList s1 = (LinkList)malloc(sizeof(Node));   /*第一個節點*/
    s1->data = 4;
    s1->next = NULL;

    head->next = s1;
    ListInsert(&head, 1, 2);     /*第1個節點前插入2*/
    ListInsert(&head, 2, 3);    /*第2個節點前插入3*/
    ListInsert(&head, 2, 7);    /*第1個節點前插入7*/
    ListInsert(&head, 3, 5);    /*第1個節點前插入5*/

    int e;
    ListDelete(&head, 3, &e);   /*刪除第3個節點*/
}

運行結果:

mark

動畫模擬:

mark

3.1.5單鏈表的整表建立

順序存儲結構的建立,其實就是一個數組的初始化;而單鏈表和順序存儲結構就不同,它所佔用的空間的大小和位置是不須要預先分配劃定的。因此建立單鏈表的過程就是一個動態生成鏈表的過程,即從「空表」的初始狀態起,依次創建各元素節點,並逐個插入鏈表。

單鏈表建立的思路:

  1. 聲明一指針p和計數變量i
  2. 初始化一空鏈表L
  3. 讓L的頭結點的指針指向NULL,即創建一個帶頭結點的單鏈表
  4. 循環
    • 生成一個新節點賦值給p
    • 隨機生成一數字賦值給p的數據域p->data
    • 將p插入到頭節點與前一新節點之間

頭插法

代碼實現:

/*頭插法*/
void CreateListHead(LinkList *L,int n)
{
    LinkList p;
    int i;

    srand(time(0));     /*初始化隨機數種子*/

    *L = (LinkList)malloc(sizeof(Node));
    (*L) -> next = NULL;    /*先創建一個帶頭結點的單鏈表*/

    for ( i = 0; i < n; i++)
    {
        p = (LinkList)malloc(sizeof(Node));     /*生成新節點*/
        p->data = rand() % 100 + 1; /*隨機生成100之內的數字*/
        p->next = (*L)->next;
        (*L)->next = p;     /*插入到表頭*/
    }
}

測試代碼:

int main()
{
    LinkList list;
    CreateListHead(&list, 5); /*建立一個有5個節點的單鏈表(不包含頭結點)*/
}

運行結果:

mark

動畫模擬:

mark

尾插法

代碼實現:

void CreateListTail(LinkList *L, int n)
{
    LinkList p,r;
    int i;

    srand(time(0));
    *L = (LinkList)malloc(sizeof(Node));
    
    r = *L;

    for (i = 0; i < n; i++)
    {
        p = (LinkList)malloc(sizeof(Node));
        p->data = rand() % 100 + 1;
        r->next = p;        /*將表尾終端節點的指針指向新節點*/
        r = p;  /*將當前的新節點定義爲表尾終端節點*/
    }

    r->next = NULL;
}

注意L和r的關係,L是指整個單鏈表,而r是指向尾節點的變量,r會隨着循環不斷的變化節點,而L則是隨着循環增加爲一個多節點的鏈表。

測試代碼:

int main()
{
    LinkList list;
    CreateListTail(&list, 5); /*建立一個有5個節點的單鏈表(不包含頭結點)*/
}

運行結果:

mark

動畫模擬:

mark

3.1.6單鏈表的整表刪除

單鏈表整表刪除的思路:

  1. 聲明一節點p和q

  2. 將一個節點賦值給p

  3. 循環

    • 將下一節點賦值給q

    • 釋放p

    • 將q賦值給p

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;
typedef struct Node
{
    ElemType data;
    struct Node *next;
} Node;
typedef struct Node *LinkList;

/*初始條件:順序線性表L已經存在*/
/*操做結果:將L重置爲空表*/
Status ClearList(LinkList *L)
{
    LinkList p, q;
    p = (*L)->next;      /*p指向第一個節點*/

    while (p)   /*沒到結尾*/
    {
        q = p->next;
        free(p);
        p = q;
    }
    (*L)->next = NULL; /*頭節點指針域爲空*/
    return OK;
}

測試代碼:

int main()
{
    LinkList list;
    CreateListTail(&list, 5); /*用尾插法建立一個5個元素的單鏈表*/
    ClearList(&list);   /*清空單鏈表*/
}

運行結果:

mark

動畫模擬:

mark

3.1.7單鏈表結構與順序存儲結構的優缺點

  • 存儲分配方式
    • 順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素
    • 單鏈表採用鏈式存儲結構,用一組任意的存儲單元存儲線性表的元素
  • 時間性能
    • 查找
      • 順序存儲結構O(1)
      • 單鏈表O(n)
    • 插入和刪除
      • 順序存儲結構O(n)
      • 單鏈表O(1)
  • 空間性能
    • 順序存儲結構須要預先分配存儲空間,分大了浪費空間,分小了容易形成內存溢出
    • 單鏈表不須要分配存儲空間,只要有就能夠分配,元素個數也不受限制

總結:若線性表須要頻繁查找,不多進行插入和刪除操做時,宜採用順序存儲結構;若線性表頻繁的進行插入和刪除操做,或者線性表中的元素個數變化較大,或者根本不知道有多大時,宜採用單鏈表結構。

3.2循環鏈表

將單鏈表中終端節點的指針由空指針改成指向頭節點,就使整個單鏈表造成一個環,這種頭尾相接的單鏈表稱爲單循環列表,簡稱循環列表(circular linked list)。

循環列表解決了一個很麻煩的問題:如何從一個節點出發,訪問到鏈表的所有節點。

非空的循環列表:

mark

循環列表帶有頭結點的空鏈表:

mark

其實循環列表和單鏈表的主要差別就在於循環的判斷條件上,單鏈表是判斷p->next是否爲空,如今則是p->next不等於頭結點,則循環未結束。

3.3雙向鏈表

雙向鏈表(double linked list)是在單鏈表的每一個節點中,再設置一個指向其前驅節點的指針域

3.3.1雙向鏈表的讀取

雙向鏈表的讀取其實和單鏈表的讀取大同小異,只不過雙向鏈表不用每一次都從頭開始找節點,支持反向查找。

mark

3.3.2雙向鏈表的插入

假設存儲元素e的節點爲s,要實現將節點s插入到節點p和p->next之間須要下面幾步,如圖所示:

mark

s->prior = p;           /*把p賦值給s的前驅,如圖①*/
s->next = p->next;      /*將p的後繼節點賦值給s的後繼,如圖②*/
p->next->prior = s;     /*將s賦值給p->next的前驅,如圖③*/
p->next = s;            /*將s賦值給p的後繼,如圖④*/

操做順序是先搞定s的前驅和後繼,再搞定後節點的前驅,最後解決前節點的後繼。順序很重要,不能顛倒

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;
/*線性表的雙向鏈表存儲結構*/
typedef struct DulNode
{
    ElemType data;
    struct DulNode *prior;      /*直接前驅指針*/
    struct DulNode *next;       /*直接後繼指針*/
} DulNode;
typedef struct DulNode *DulLinkList;

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操做結果:在L中第i個節點位置以前插入新的數據元素e,L的長度加1*/
Status DulListInsert(DulLinkList *L, int i, ElemType e)
{
    int j;
    DulLinkList p = *L;
    j = 1;

    while (p && j<i)    /*尋找第i-1個節點*/
    {
        p = p->next;
        ++j;
    }
    if (!p || j > i)
    {
        return ERROR;   /*第i個節點不存在*/
    }
    DulLinkList s = (DulLinkList)malloc(sizeof(DulNode));   /*生成新節點*/

    s->data = e;
    s->prior = p;               /*把p賦值給s的前驅*/
    s->next = p->next;      /*將p的後繼節點賦值給s的後繼*/
    p->next->prior = s;     /*將s賦值給p->next的前驅*/
    p->next = s;                    /*將s賦值給p的後繼*/
    return OK;
}

測試代碼:

int main()
{
    DulLinkList dulList;
    CreateDulListHead(&dulList, 5); /*初始化一個有5個節點的循環鏈表*/
    DulListInsert(&dulList, 3, 7);/*在循環鏈表第3個節點前插入數據7*/
}

運行結果:

mark

咱們能夠看出循環鏈表一個節點的前驅的後繼或者後繼的前驅都是它本身

p->next->prior = p = p->prior->next

動畫模擬:

mark

3.3.3雙向鏈表的刪除

若是插入操做理解了,那麼刪除操做就很簡單了。

假設要刪除節點p,須要下面兩步,如圖所示:

mark

p->prior->next = p->next;   /*將p->next賦值給p->prior的後繼,如圖①*/
p->next->prior = p->prior;  /*將p->prior賦值給p->next的前驅,如圖②*/

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;
/*線性表的雙向鏈表存儲結構*/
typedef struct DulNode
{
    ElemType data;
    struct DulNode *prior;      /*直接前驅指針*/
    struct DulNode *next;       /*直接後繼指針*/
} DulNode;
typedef struct DulNode *DulLinkList;

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操做結果:刪除L中第i個節點,並用e返回其值,L的長度減1*/
Status DulListDelete(DulLinkList *L, int i, ElemType *e)
{
    int j;
    DulLinkList p = *L;
    j = 1;
    while (p->next && j<i)  /*尋找第i-1個節點*/
    {
        p = p->next;
        ++j;
    }
    if (!(p->next) || j > i)
    {
        return ERROR;       /*第i個節點不存在*/
    }
    DulLinkList q = p->next;
    q->prior->next = q->next;       /*將q->next賦值給q->prior的後繼*/
    q->next->prior = q->prior;  /*將q->prior賦值給q->next的前驅*/
    *e = q->data;   /*將q節點中的數據給e*/

    free(q);        /*回收此節點,釋放內存*/

    return OK;
}

測試代碼:

int main()
{
    DulLinkList dulList;
    ElemType e;
    CreateDulListHead(&dulList, 5); /*初始化一個有5個節點的循環鏈表*/
    DulListDelete(&dulList, 3, &e); /*刪除循環鏈表第3個節點並賦值給e*/
}

運行結果:

mark

動畫模擬:

mark

3.3.4雙向循環鏈表

既然單鏈表能夠有循環鏈表,那麼雙向鏈表固然也能夠是循環鏈表。

雙向鏈表的循環帶頭節點的空鏈表

mark

雙向鏈表的循環帶頭節點的非空鏈表

mark


本文爲博主學習感悟總結,水平有限,若是不當,歡迎指正。

若是您認爲還不錯,不妨點擊一下下方的推薦按鈕,謝謝支持。

轉載與引用請註明出處。

相關文章
相關標籤/搜索