1. 程式人生 > >重溫資料結構(二)

重溫資料結構(二)

嘿嘿嘿,哈哈哈(摩拳擦掌中)。

今天的工作做完了,讓我們開始繼續看《大話資料結構》。 ^ - ^

今天要看的是線性表。最簡單也最常用。它的英文名是List。

線性表:零個或多個數據元素的有限序列。

當線性表裡的元素為零個的時候,就稱為空表。

線性表主要分為順序儲存和鏈式儲存,先來看看簡單一點的順序儲存。

一.線性表的順序儲存

線性表的順序儲存:指的是用一段地址連續的儲存單元依次儲存線性表的資料元素。

下面來看看順序儲存的結構程式碼:

#define MAX_SIZE 100  //儲存空間初始分配量
typedef int ElemType;   //ElemType 現定義為int
typedef strut{ ElemType data[MAX_SIZE]; //陣列儲存元素 int length; //線性表當前長度 }SqList; //別名SqList

特別注意,length是表示當前線性表的長度,是可變的,但length不會超過data的最大長度。

下面就可以來看看基本的操作啦:

1.查詢操作

// 得到線性表L中第i個數據
void GetElem (SqList L , int i ,ElemType *e){
    int currentLength = L.length;//當前線性表長度
    if(currentLength == 0
|| i < 1 || i > currentLength){ //空表,越界 }else{ *e = L.data[i-1]; //得到陣列中下標為i-1的值 } }

注:預設線性表的長度從1開始,而預設的陣列下標從0開始,所以線性表中第i個數據對應陣列下標為i-1的資料。

2.插入操作

//線上性表中的第i個位置插入新元素e
void ListInsert(SqList *L , int i ,ElemType e){
    int currentLength = L->length;
    if(currentLength == MAX_SIZE || i < 1
|| i > currentLength + 1){ //表滿,越界 }else{ if(i <= currentLength){//插入位置不在表尾 for(int k = currentLength - 1; k >= i-1 ; k--){ L->data[k+1] = L->data[k];//把要插入位置後的元素向後移動 } }//end if L->data[i-1] = e; //插入新元素e L->length++; //線性表長度+1 }//end if }

注:把元素向後移的時候,從表尾開始,一直到陣列下標為i-1為止。插入新元素e後,記得將線性表長度+1。

2.刪除操作

//刪除線性表的第i個位置的元素
void ListDelete(SqList *L,int i,ElemType *e){
    int currentLength = L->length;
    if(currentLength == 0 || i > currentLength || i < 1){
        //空表,越界
    }else{
        *e = L->data[i-1];//需要可用於儲存資料,不需要則去除
        if(i < currentLength){//如果i不在表尾
            for(int k = i -1; k < currentLength - 1; k++){
                L->data[k] = L->data[k+1];//把刪除位置後的元素向前移動
            }
            L->length--;//線性表長度-1
        }//end if
    }//end if
}

注:把元素向前移動的時候,從刪除位置i對應的下標i-1開始一直到表尾,然後把線性表的長度-1。

下面我們來分析一下線性表查詢插入刪除的時間複雜度。

查詢的時間複雜度

直接查詢到對應下標的資料,時間複雜度很明顯為O(1)。

插入和刪除的時間複雜度

最好的情況:插入和刪除的位置都在表尾,則不用進行元素的移動,時間複雜度為O(1)。

最壞的情況:插入和刪除的位置都在表首,則需要將整個表的元素向前移動或者向後西東,時間複雜度為O(n)。

平均的情況:插入和刪除需要移動n-i個元素,平均概率下,最終的移動次數就與最中間的元素的移動次數相等,為n-1/2 。根據上一章的大O階推導法,可得時間複雜度仍為O(n)。

最後來概括一下線性表的順序儲存結構的優缺點:

優點:

1.無須為表示表中元素之間的邏輯關係而增加額外的儲存空間(一開始就定義了最大的儲存空間)
2.可以快速地存取表中的任意位置的元素

缺點:

1.插入和刪除操作需要移動大量元素
2.當線性表長度變化較大時,難以確定儲存空間的容量
3.造成儲存空間的“碎片”(所謂碎片是指沒有用到的陣列空間)

今天就先到此為止啦,下班咯  ^ - ^ ————2016.7.29 18:00

下面給出測試程式碼:

//為了使結果顯而易見,將List輸出
void PrintList(SqList L){
    cout<<"SqList: ";
    for (int i =0; i< L.length; i++)
            cout<<L.data[i] <<" ";
    cout<<" length  is "<<L.length<<endl;
}
int main()
{
    //線性表初始化
    SqList L;
    int n = 8for (int i =0; i< n; i++)
        L.data[i] = i*i;
    L.length = n;
    PrintList(L);

    //查詢第i個位置的元素
    int e;
    int i = 4;
    GetElem(L,i,&e);
    cout<<"get "<<i<<" is "<<e<<endl;

    //在第i個位置插入e
    i = 2;
    e = 99;
    ListInsert(&L,i, e);
    cout<<"insert into "<<i<<" is "<<e<<endl;
    PrintList(L);

    //刪除第i個位置的元素
    i = 3;
    ListDelete(&L,i,&e);
    cout<<"delete from "<<i<<" is "<<e<<endl;
    PrintList(L);
    return 0;
}

下面給出測試程式碼的執行結果:

執行結果

注:如果想下載完整程式碼的可以去我的資源頁下載。

二.線性表的鏈式儲存

首先我們需要知道一個名詞——結點,英文名Node,一個Node裡面包含了資料域和指標域,資料域用來儲存資料元素的資訊,指標域記憶體儲的資訊可以稱為指標或鏈,用來指示其直接後繼的資訊。

n個Node鏈結成一個連結串列,即為線性表的鏈式儲存結構,因為每個Node只包含一個指標域,又可以稱為單鏈表。

單鏈表

圖1是空連結串列,頭結點的後繼指標地址為null。

圖2是帶頭結點的單鏈表,其中頭結點的功能,以及和頭指標的區別我們會在下面仔細說明。一般使用線性表,都是指有頭結點的情況。

圖3是不帶頭結點的單鏈表。

頭指標:

1.線上性表的鏈式儲存結構中,頭指標是指連結串列指向第一個結點的指標,若連結串列有頭結點,則頭指標就是指向連結串列頭結點的指標。
2.頭指標具有標識作用,故常用頭指標冠以連結串列的名字。
3.無論連結串列是否為空,頭指標均不為空。頭指標是連結串列的必要元素。

頭結點:

1.頭結點是為了操作的統一與方便而設立的,放在第一個元素結點之前,其資料域一般無意義(當然有些情況下也可存放連結串列的長度、用做監視哨等等)。
2.有了頭結點後,對在第一個元素結點前插入結點和刪除第一個結點,其操作與對其它結點的操作統一了。
首元結點也就是第一個元素的結點,它是頭結點後邊的第一個結點。
3.頭結點不是連結串列所必需的。

加入頭結點有什麼好處呢?
加了頭結點之後,插入、刪除都是在後繼指標next上進行操作,不用動頭指標;
若不加頭結點的話,在第1個位置插入或者刪除第1個元素時,需要動的是頭指標。
例:在進行刪除操作時,L為頭指標,p指標指向被刪結點,q指標指向被刪結點的前驅,對於非空的單鏈表:
1.帶頭結點時
刪除第1個結點(q指向的是頭結點):q->next=p->next; free(p);
刪除第i個結點(i不等於1):q->next=p->next;free(p);
2.不帶頭結點時
刪除第1個結點時(q為空):L=p->next; free(p);
刪除第i個結點(i不等於1):q->next=p->next;free(p);
結論:帶頭結點時,不論刪除哪個位置上的結點,用到的程式碼都一樣;
不帶頭結點時,刪除第1個元素和刪除其它位置上的元素用到的程式碼不同,相對比較麻煩。

下面來看看鏈式儲存的結構程式碼:

typedef int ElemType;   //ElemType 現定義為int
typedef struct Node{ //這個Node一定要寫
    ElemType data; //儲存的資料
    Node *next;//後繼指標
}Node;//別名為Node

typedef Node *LinkList;  //定義LinkList

注:data就是資料域,next就是指標域

下面就可以來看看基本的操作啦:

1.建立LinkList

//建立帶表頭結點的單鏈表 (從頭插入)
void CreateLsit(LinkList *L , int n){
    LinkList p ;
    *L = (LinkList)malloc(sizeof(Node));
    (*L)->next = NULL ;//一個帶頭結點的單鏈表
    for(int i = 0;i < n ; i++){
        p = (LinkList) malloc (sizeof(Node));
        p->data = i*i;

        //從頭結點插入
        p->next  = (*L)->next;
        (*L)->next = p;
    }
}

思路:這個比較簡單,直接從頭結點插入就好了,插入的詳細操作在下面會講解。

//建立帶表頭結點的單鏈表(從尾插入)
void CreateLsit2(LinkList *L , int n){
    LinkList p , r;
    *L = (LinkList)malloc(sizeof(Node));
    r = (*L);//尾指標等於頭指標
    for(int i = 0;i < n ; i++){
        p = (LinkList) malloc (sizeof(Node));
        p->data = i*i;

        //從尾部插入
        r->next = p;
        r = p;
    }
    r->next = NULL;//最後尾指標指向null
}

思路:新建一個尾結點r,在開始的時候令r=(*L),插入的時候直接令r->next 等於新插入的結點p , 同時將r向後移動,在最後的時候令r->next = NULL;

2.查詢操作

//查詢LinkList中第i個數據
void GetElem(LinkList L,int i,ElemType *e){
    LinkList p;
    int j = 1;
    p = L->next;  //連結串列L指向的第一個結點
    while( p && j < i){
        p = p->next;
        j++;
    }//end while
    if(!p  ||  j > i){
        //第i個元素為空,越界
    }else{
        *e = p->data;
    }//end if
}

思路:
1.建立一個新結點p,讓p指向連結串列的第一個結點,j作為當前位置,從1開始
2. 當p不為null,並且j<i時,遍歷連結串列,讓p的指標向後移動,j++
3. 如到連結串列末尾p為空,則說明第i個元素不存在
4. 否則,講結點的資料賦值給e

3.插入操作

//在第i個位置之前插入e
void ListInsert(LinkList *L , int  i, ElemType e){
    int j = 1;
    LinkList p,s;
    p = *L;
    while(p && j < i){
        p = p->next;
        j++;
    }
    if(!p ||  j > i ){
        //第i個位置,越界
    }else{
        s = (LinkList)malloc(sizeof(Node));
        s->data = e;
        s->next = p->next;
        p->next = s;
    }
} 

思路:
1.建立一個新結點p,讓p指向連結串列的第一個結點,j作為當前位置,從1開始
2. 當p不為null,並且j<i時,遍歷連結串列,讓p的指標向後移動,j++
3. 如到連結串列末尾p為空,則說明第i個元素不存在
4. 否則,建立一個新結點s,將e賦值給s->data,然後就是單鏈表插入標準語句:s->next = p->next; p->next = s;

4.刪除操作

//刪除第i個元素
void ListDelete(LinkList *L , int  i, ElemType *e){
    int j = 1;
    LinkList p,q;
    p = *L;
    while(p->next && j < i){
        p = p->next;
        j++;
    }
    if(!(p->next) ||  j > i ){
        //第i個位置,越界
    }else{
        q = p->next;     //q為臨時變數
        p->next = q->next;
        *e = q->data;
        free(q);
    }
} 

思路:
1.建立一個新結點p,讓p指向連結串列的第一個結點,j作為當前位置,從1開始
2. 當p->next不為null,並且j<i時,遍歷連結串列,讓p的指標向後移動,j++
3. 如到連結串列末尾p為空,則說明第i個元素不存在
4. 否則,建立一個臨時結點q,令q=p->next,把q->data值賦給e,然後就是單鏈表刪除標準語句:p->next = q->next;最後把q結點用free釋放。

下面我們來分析一下線性表查詢插入刪除的時間複雜度。

查詢的時間複雜度

從頭結點開始找,一直到i位置結束,時間複雜度為O(n)。

插入和刪除的時間複雜度

從頭結點開始找,一直到i位置,再進行插入刪除操作,時間複雜度仍為O(n)。這樣看來,似乎鏈式儲存和順序儲存沒有在時間複雜度上沒有什麼區別。但如果我要在第i個位置同時插入多個元素,順序儲存每次的時間複雜度都是o(n),而鏈式儲存只有在查詢第i個位置的時候,時間複雜度為o(n),其餘的只需通過賦值和移動指標實現,時間複雜度都是o(1).
由此我們可得出結論:在刪除插入操作頻繁的時候,鏈式儲存的優勢就更明顯。

下面給出測試程式碼:

//輸出LinkList
void PrintList(LinkList L){
    LinkList p = L->next;
    cout<<"LinkList: ";
    int j=0;
    while (p){
        cout<<p->data<<"  ";
        p=p->next;
        j++;
    }
    cout<<" length is "<<j<<endl;
}
int main(){
    LinkList L,L1;
    //從頭插入建立LinkList
    CreateLsit(&L,10);
    PrintList(L);
    //從尾插入建立LinkList
    CreateLsit2(&L1,10);
    PrintList(L1);
    //清楚表
    ClearList(&L);
    PrintList(L);
    //插入
    int i = 5;
    int e = 99;
    ListInsert(&L1,i,e);
    cout<<"insert into "<<i<<" is "<<e<<endl;
    PrintList(L1);
    //查詢
    GetElem(L1,i,&e);
    cout<<"get "<<i<<" is "<<e<<endl;
    //刪除
    ListDelete(&L1,i,&e);
    cout<<"delete from "<<i<<" is "<<e<<endl;
    PrintList(L1);

    return 0;
}

下面給出測試程式碼的執行結果:

單鏈表測試

注:如果想下載完整程式碼的可以去我的資源頁下載。

Over ————2016.7.30 16:00