數據結構中的線性表,期末不掛科指南,第2篇

線性表

這篇博客寫的是線性表相關的內容,包括如下部分,先看下有木有期待

  1. 啥是線性表
  2. 線性表的順序存儲
  3. 線性表的基本運算在順序表上的實現
  4. 線性表的鏈式存儲
  5. 線性表的基本運算在單鏈表上的實現
  6. 循環鏈表與雙向循環鏈表

Over,內容還蠻多的!~  ̄□ ̄||,頭大了…

首先明確一個非常重要的點

線性表是一個線性結構,注意上篇博客提過線性結構是數據的邏輯結構中的一種

基本概念

線性表是由n(n≥0)個數據元素組成的有窮序列

大白話:在內存上一個個排着,找到一個,剩下的挨着找就行

數據元素又稱作結點

吐槽:人類在創造術語的路上就是這麼帶勁,上節課剛說數據元素又稱元素,這又來一個結點,得,記住吧

結點個數叫做表長,那麼我們用一張完整的圖來說明一下
在這裏插入圖片描述
線性表的基本運算,需要了解一下

  1. 初始化 Initiate(L)
  2. 求表長 Length(L)
  3. 讀表元素 Get(L,i)
  4. 定位 Locate(L,i)
  5. 插入Insert(L,x,i)
  6. 刪除Delete(L,i)

線性表的順序存儲

用順序存儲實現的線性表稱爲順序表。一般使用數組來表示順序表

接下就是刺激的時刻了,比較難的部分來了,因爲要用C來實現線性表的基本運算

首先假定線性表的數據元素的類型爲DataType ,這個DataType 可以是自定義的,也可以是默認的int,char等類型

const int Maxsize = 100 ;  // 預先定義一個足夠大的常數
typedef struct{
	DataType data[Maxsize]; // 存放數據的數組
	int length ; // 順序表的實際長度
} SeqList; // 順序表類型名爲SeqList  
SeqList L ;  // 定義一個L爲順序表

實現插入操作,函數名字爲InsertSeqList(SeqList L,DataType x,int i) 表示在順序表第i(1≤i≤n+1)個元素之前,插入一個新元素。使得線性表長度加1。

上面是邏輯上的C語言實現,接下來咱們先引用一個圖,說明一下如何用C語言在內存上開闢一塊空間,並且向裏面存數據

#include <stdio.h>
#include <stdlib.h>

const int Maxsize = 10;
typedef struct SeqList{
    int *data; //一個int指針,後面用來初始化數組用
    int length;
} seq;

// 順序表的初始化函數
seq init(){
    seq s;
    s.data = (int*)malloc(Maxsize*sizeof(int)); // 構造一個空的順序表,動態申請存儲空間
    if(!s.data) // 如果申請失敗,退出程序
    {
        printf("初始化失敗");
        exit(0);
    }
    s.length = 0; // 空表的長度初始化爲0
    return s;
}

上述代碼,相當於在內存上做了圖示的操作
在這裏插入圖片描述
開闢空間之後,向每個小格子裏面添加數字

void display(seq s){
    for(int i=0;i<s.length;i++){
        printf("%d",s.data[i]);
    }
    printf("\n");
    
}

int main()
{
    seq s = init();
    //添加一個元素進入
    for(int i=1;i<=5;i++){
        s.data[i-1] = i;
        s.length++;
    }
    printf("初始化之後,表的數據爲:\n");
    display(s);

    return 0;
}

可以看動畫理解
在這裏插入圖片描述
添加元素完成之後,就是刪除元素

刪除的基本步驟

  1. 結點ai+1,…an依次向左移動一個元素位置
  2. 表長度減1

看一下代碼吧

seq delete_seq(seq s,int i){
    if(i<1||i>s.length){
        printf("位置錯誤");
        exit(0);
    }
    
    // 第i個元素下標修改爲i-1
    for(int j=i;j<s.length;j++){
        s.data[j-1] = s.data[j];
    }
    
    s.length--;
    return s;
}

接下來實現定位的算法,說白了,就是判斷一個值(x)的位置(i)

C語言的代碼如下

// 注意,這個地方需要返回的爲int了,也就是位置
int locate(seq s,int x){
    int i =0;
    while((i<s.length)&&(s.data[i]!=x)){
        i++;
    }
    if(i<s.length) return i+1;
    else return -1;
}
 

線性表的順序存儲的時間複雜度

運算 插入 刪除 定位 求表長 讀取元素
時間複雜度 O(n) O(n) O(n) O(1) O(1)

具體是怎麼來的,需要你自己看看算法的實現嘍,通過上述表格知道
順序表的插入、刪除算法在時間性能方面不是很理想,接下來我們就採用線性表的鏈接存儲來看一下,是否存在優化。

線性表的鏈接存儲

鏈式存儲結構,上來需要記住有三種常見的 單鏈表循環鏈表雙向循環鏈表

首先明確,單鏈表中每個結點由兩部分組成
在這裏插入圖片描述

  • data表示數據域
  • next表示指針域鏈域

一些簡單的結點概念
在這裏插入圖片描述

線性表在單鏈表上實現基本運算

接下來重頭戲來了,我們要用代碼實現一個簡單的單鏈表

空的單鏈表由一個頭指針和一個頭結點組成

初始化

初始化之前,我們需要先用C語言定義一個新的結構體

//鏈表中的每個結點的實現
//包含數據域與指針域
typedef struct node{
	int data;// 數據域,爲了計算方便,類型設置爲數字
	struct node *next; // 指針域,指向後繼元素
} Node,*LinkList;

結構體定義好了之後,就可以開始初始化操作了
頭結點初始化其實就是在內存上開闢一塊空間,然後將指針域指向NULL
在這裏插入圖片描述
請看代碼,注意返回的是一個指針類型,說白了就是頭結點的地址

// 初始化
LinkList init(){
    Node *L; // 定義一個頭結點
    L =(LinkList)malloc(sizeof(Node)); //頭結點申請地址
    if(L == NULL){
        printf("初始化失敗!\n");
        exit(0);
    }
    L->next =NULL;
    return L;
}

初始化成功,開始插入元素

插入元素,有頭插入、尾插、任意插

先說一下頭插,當頭結點初始化完畢之後,第一個元素插入進來就比較簡單了,看動圖
在這裏插入圖片描述
這是插入一個元素,在用頭插法插入第二個元素呢?
在這裏插入圖片描述
新生成的pnew2首先將自己的指針域指向頭結點的指針域pnew2->next = L.next,然後L.next = pnew2 即可

上述的邏輯寫成代碼如下

// 頭插入法
void insert_head(Node *L){
    int i,n,num;  // n表示元素的個數 
    Node *pnew;
    printf("請輸入要插入的元素個數:n = ");
    scanf("%d",&n);
    for(i=0;i<n;i++){
        printf("請輸入第%d個元素: ",i+1);
        scanf("%d",&num);
        pnew = (LinkList)malloc(sizeof(Node));
        pnew->data = num; // 將數字存儲到數據域
        pnew->next = L->next; // 指針域指向L(頭結點)的指針域
        L->next = pnew; // 頭結點指針域指向新結點地址
        
    }
}

接下來看一下尾插法,其實理解起來也不難,說白了就是在鏈表後面追加元素即可
在這裏插入圖片描述
代碼如下,這個地方看一下里面有一個p=L請問直接使用L可以嗎?爲什麼不直接用,搞清楚了,你也就明白了

// 尾插法
void insert_tail(Node *L){
    int i,n,num;
    Node *p,*pnew;
    p = L;
    
    printf("要輸入元素的個數:n = ");
    scanf("%d",&n);
    for(i=0;i<n;i++){
        
        printf("請輸入第%d個元素:",i+1);
        scanf("%d",&num);
        pnew = (LinkList)malloc(sizeof(Node));
        if(pnew == NULL){
            printf("初始化失敗");
            exit(0);
        }
        pnew->data = num;
        p->next = pnew;
        p = pnew;
        
    }
    p->next = NULL;
  
}

剩下的算法實現就比較簡單了,例如求表長,通過循環的方式,計算一下即可

//求表長
int get_length(LinkList L){
    LinkList p;
    int length = 0;
    p = L->next;   // p 指向第一個結點
    while(p){
        printf("單鏈表的數據爲%d\n",p->data);
        length++;
        p = p->next;
    }
    return length;
}

讀表中的元素

// 讀表中的元素
LinkList get_element(LinkList L,int i){
    // 在單鏈表L中查找第i個結點,若找到,則返回指向該結點的指針,否則返回NULL
    Node *p;
    p = L->next;
    int position = 1;
    while((position<i)&&(p!=NULL)){ // 當未到第i結點且未到尾結點時繼續後移
        p = p->next;
        position++;
    }
    if(i==position) return p; //找到第i個結點
    else return NULL;   // 查找失敗
}

讀取表的元素,還可以按照值去找,返回位置,嘗試一下吧,寫起來都是比較容易的

int get_element_by_data(LinkList L,int x){
    Node *p;
    p = L->next;
    int i = 0;
    while(p!=NULL && p->data == x){
        p = p->next;
        i++;
    }
    if (p!=NULL) return i+1;
    else return 0;
}

寫個複雜點的,在任意位置插入一個元素,這個還是好玩一些的

/在任意位置插入元素,x爲要插入的內容,i爲插入的位置
void insert(LinkList L,int x,int i){
    
    Node *p,*q; //p表示要插入的元素,q表示要被插入的元素
    if(i==1) q = L; //如果i==1,那麼p=L進行初始化操作
    else q = get_element(L,i-1); // 找到第i-1個元素
    
    if(q==NULL){
        printf("找不到位置");
        exit(0);
    }
    else{
         p =(LinkList)malloc(sizeof(Node));
         p->data = x;
         p->next = q->next; // 新生成的p指向q的下一個結點
         q->next = p;//q的指針域指向新生成的p
        
    }
}

簡單說明一下吧
大白話爲 要在第i個位置插入一個元素x,那麼需要找到i-1位置的元素,這個元素叫做 q

讓新元素p(數據域爲x,指針域爲空)的指針域指向第i 元素,也就是q原先的指針域,防止丟失掉

然後在叫q的指針域指向p的地址即可,如果還不明白,看圖

在這裏插入圖片描述
對於刪除任意位置的節點,這個就要留給你自己了

如果將ai移除鏈表,需要找到直接前驅,讓直接前驅的指針域指向ai+1的地址就可以了

記得,通過free(p)釋放結點

刪除全部結點也需要自己完成一下,儘量把代碼寫完哦~~~

單鏈表的時間複雜度

  • insert(LinkList L,int x,int i) 時間複雜度爲O(n2)
  • 頭插法和尾插法時間複雜度爲O(n)

循環鏈表

環狀鏈表只需要將表中最後一個結點的指針指向頭結點,鏈表就形成了一個環
如圖
在這裏插入圖片描述
循環鏈表如何想研究一下可以去實現約瑟夫環,由於本教材中不是重點,所以選修即可

雙向循環鏈表

雙向循環鏈表就是在單鏈表中的每個結點在增加一個指向直接前驅的指針域prior ,這樣每個結點就有兩個指針了
在這裏插入圖片描述
注意點

  1. 雙向循環鏈表是一種對稱結構,即可以直接訪問前驅結點又可以直接訪問後繼結點,所以找前驅和後繼結點的時間複雜度都是O(1),也可以得到結論雙向循環鏈表適合應用在需要經常查找結點的前驅和後繼場合
  2. p = p->prior->next = p->next->prior

教材中重點給出了刪除和插入的兩個邏輯,我們看一下

// p表示的是待刪除的結點
p->prior->next = p->next;
p->next->prior = p->prior;
free(p)

圖示如下
在這裏插入圖片描述
大白話
先讓p等於要刪除的結點,然後把p刪除前,需要將p的前驅和後繼結點連接起來,剛纔的代碼乾的就是這個事情!

插入邏輯

在p所指的結點後面插入一個新結點*t,需要修改四個指針:

t->prior = p;
p->next = t;  // 這兩個步驟將t和p連接起來了

t->next = p->next;
p->next->prior = t; //這兩個步驟將t和p後繼結點連接起來了


期末考試

這章是期末考試或者自考的一個比較重要的考試章節,一般會出現算法設計題,難度係數挺高的

建議,在能力範圍內用C語言實現順序表的基本運算,實現單鏈表的基本運算

懵了吧,嘿嘿~,多看幾遍,多看幾遍,看圖,看圖,寫代碼,運行,運行

歡迎關注,夢想橡皮擦公衆號哦~
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章