1. 程式人生 > >資料結構 --- 線性表 順序儲存 鏈式儲存

資料結構 --- 線性表 順序儲存 鏈式儲存

線性表是平時一直會用到的資料結構,像python裡面的list這種高階資料結構,其實也是對這種底層結構的封裝。

這篇文章寫了整整4天........

線性表的儲存結構主要分兩大類,一類一類來看。

在這之前,先用虛擬碼來形容一下線性表擁有的基本功能


1 :順序儲存結構

聽名字就知道,這是按照順序來排的,簡單來說,就是在用順序儲存結構來建立線性表的時候,他是在記憶體裡面先申請一塊空地

然後,所有的相同型別的資料元素按照順序,放進去,並且,在記憶體中的存放,也是連續的!

也就是打個比方a,b,c三個元素的記憶體地址,如果按照順序儲存結構的話就會是0x00001 ---> 0x00002 ---> 0x00003

另外,順序儲存的時候,由於是先申請記憶體空間,再放入線性表,所以,正常來說,陣列長度(也就是申請的空間大小)是大於等於線性表長度的

其次,陣列內元素的下標,是要比第i個元素的i少一位的,如下圖.


概括一下,線性表的儲存結構可以用以下程式碼來抽象表示

#define MAXSIZE 20 /* 儲存空間初始分配量 */

typedef struct
{
	ElemType data[MAXSIZE];        /* 陣列,儲存資料元素 */
	int length;                                /* 線性表當前長度 */
}SqList;

每個資料元素,其實在記憶體中都有他的固定地址的

然後由於順序線性表的都是連續固定的,所以根據每種元素的資料型別的不同,你可以看出後面元素的不同記憶體地址

假設我們存放的是整數int,他每個元素要佔用c個空間,那麼我們用LOC(a)來表示a這個元素的記憶體地址的話

那麼LOC(ai+1)=LOC(ai)+C

那麼LOC(ai)可以根據a1的LOC來推算出他的記憶體地址LOC(ai)=LOC(a1)+(i-1)*c


所以,由於記憶體地址都是連續的,所以你完全可以通過第一個元素就知道其中第i個元素的值,所以,讀取或存放的時間複雜度為O(1)

1 順序儲存線性表的元素獲取

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0

typedef int Status;          /* Status是函式的型別,其值是函式結果狀態程式碼,如OK等 */

/* 初始條件: 順序線性表L已經存在,1≤i≤ListLength(L) */

/* 操作結果: 用e返回L 中第i個數據元素的值 */

Status GetElem(SqList L,int i,ElemType *e)   /* 這裡等於是定義GetElem返回時整型 ,這句函式其實等於int GetElem  */
{
    if(L.length==0 || i<1 || i>L.length)
            return ERROR;
    *e=L.data[i-1];

    return OK;
}

如果陣列長度為0 或 i小於1 或 i 大於陣列長度最大位的後一位,直接返回ERROR

否則,代表能讀到這個數,則給 *e 賦值,值為陣列資料的第i-1下標位的數值,並返回OK

2 順序儲存線性表的元素插入

這個原理和插隊是一樣的,比如一共10個人的隊伍,你認識第4個人,你讓他給你插下隊,那你認識的人就變成第5個了,你變成了第4個,佇列長度要加1

/* 初始條件:順序線性表L已存在,1≤i≤ListLength(L), */
/* 操作結果:在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--)  /* 將要插入位置之後的資料元素向後移動一位,k>=i-1等於是說明,當新元素的下標k,越過i-1時 */
			L->data[k+1]=L->data[k]; /*每次歷遍都把當前下標位k的元素向後移動一位*/
	}
	L->data[i-1]=e;          /* 將新元素插入 */
	L->length++;

	return OK;
}

這裡注意,溫習一下,指標訪問結構體成員的方法是L->length,如果是結構體變數訪問成員的話,是 xxx.length

3 順序儲存線性表的元素刪除

/* 初始條件:順序線性表L已存在,1≤i≤ListLength(L) */
/* 操作結果:刪除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];                /* 用第i個元素給e賦值 */
    if (i<L->length)                /* 如果刪除不是最後位置 */
    {
        for(k=i;k<L->length;k++)/* 將刪除位置後繼元素前移 */  
			L->data[k-1]=L->data[k];  /* 通過元素下標前移元素 */
    }
    L->length--;
    return OK;
}

通過插入元素和刪除元素,我們可以看到,需要對元素進行操作的話,最壞情況下,是每個元素都要移動一下。

所以這時候的時間複雜度是O(n)

至此可以看出順序儲存結構線性表的優缺點了


2.鏈式儲存結構線性表

鏈式儲存結構和順序結構正好相反,他的元素存放,不是排排坐吃果果般連續的,而是分散的,說不定是記憶體地址0x00001------>0x00013------>0x00022

但是,前一個元素,會知道後一個元素的記憶體地址,他是如何知道的呢

這是因為,鏈式儲存結構的元素,不光光有資料值,他還有指標域,指向後一個元素

這樣,我們把資料域+指標域稱為結點Node


每個結點只包含一個指標域的,叫做單鏈表

作為單鏈表來說,總得有一個指向第一個元素結點的東東,這裡,叫做頭指標,單鏈表裡面必須有的東西。

頭指標只有指標域,他指向的是第一個結點的地址.

而相對的,最後一個元素結點的指標域,指向的結果是NULL

另外,有時我們還會在頭指標和第一個元素結點的中間,新增一個頭結點,這個結點的資料域可以不存放東西,也可以存放如陣列長度啊這些的資訊,他的指標域指向第一個元素結點。



然後來看下頭指標和頭結點的區別

這樣,總結一下儲存示意圖

其實對於連結串列來說,他都是由一個一個結點組成的

所以,我們可以用結構指標來表示連結串列

typedef struct Node
{
    ElemType data;            /* 資料域 */
    struct Node *next;        /* 指標域 */
}Node;                        /* 取個別名Node,方便操作 */
typedef struct Node *LinkList; /* 定義LinkList,指向結構體Node的指標 */

假設我們有個指標p,指向連結串列的第i個結點,那麼p->data就表示資料域,p->next就表示指標域

p->next指向的是下一個結點,如果要指向下一個結點的資料域,則是表示成p->next->data


1 單鏈表的讀取

對於單鏈表來說,你要獲取一個元素,你必須從頭開始歷遍

/* 初始條件:順序線性表L已存在,1≤i≤ListLength(L) */
/* 操作結果:用e返回L中第i個數據元素的值 */
Status GetElem(LinkList L,int i,ElemType *e)
{
	int j;
	LinkList p;		/* 宣告一指標p */
	p = L->next;		/* 讓p指向連結串列L的第一個結點 */
	j = 1;		/*  j為計數器 */
	while (p && j<i)  /* p不為空或者計數器j還沒有等於i時,迴圈繼續 */
	{   
		p = p->next;  /* 讓p指向下一個結點 */
		++j;
	}
	if ( !p || j>i ) 
		return ERROR;  /*  第i個元素不存在 */
	*e = p->data;   /*  取第i個元素的資料 */
	return OK;
}

這裡需要注意一下,上一個章節裡面講順序儲存結構線性表的時候,SqList是一個結構體別名, 他定義生成的物件是例項變數

而這個章節的LinkList,是一個指向結構體Node的指標!他定義生成的物件是一個指標

還有p=L->next不是將L的下一個結點賦值給p,而是將p指向L的下一個結點,p還是指標型別

上面這個獲取元素的程式碼,他在 j 未達到 i 的過程中,迴圈將p結點向後移動,他將在  j=i 的時候, 將當前p的data,也就是儲存的數值,賦值給 *e

最後return Ok

而通過連結串列的結構我們可以知道,只有上一個結點才知道下一個結點在哪裡,所以,當你需要知道一個結點的值的時候,只能從開頭一個一個歷遍

所以,他的時間複雜度是O(n)

2 單鏈表的插入

事物總有兩面性,連結串列的讀取的時間複雜度是O(n) ,那肯定也有優點,來看看單鏈表的插入

簡單來說,連結串列的插入就是,把新結點的後繼指標指向原來這個位置的結點,再把前一個結點的後繼指標指向新結點

先後次序可以概括為  s->next = p->next ,然後 p->next = s

這裡得記住,千萬不能顛倒次序,如果你先將p->next=s進行了賦值,那後面s->next就等於他s自己了,等於斷鏈了。

    

/* 初始條件:順序線性表L已存在,1≤i≤ListLength(L), */
/* 操作結果:在L中第i個位置之前插入新的資料元素e,L的長度加1 */
Status ListInsert(LinkList *L,int i,ElemType e)
{ 
	int j;
	LinkList p,s;
	p = *L;                /* p還是指標,指向第一個結點 */
	j = 1;
	while (p && j < i)     /* 尋找第i個結點 */
	{
		p = p->next;   /* 這句很重要,他並不是說p是p的下一個結點,而是說,p從指向當前結點,變成指向下一結點 */
		++j;
	} 
	if (!p || j > i) 
		return ERROR;   /* 第i個元素不存在 */
	s = (LinkList)malloc(sizeof(Node));  /*  生成新結點(C語言標準函式) */
	s->data = e;  
	s->next = p->next;      /* 將p的後繼結點賦值給s的後繼  */
	p->next = s;          /* 將s賦值給p的後繼 */
	return OK;
}

這段程式碼再附加解釋一下

LinkList p,s是定義了2個結構體指標p和s

並通過p=*L的語句,讓p指第一個結點, 具體意思是,傳入的是頭指標本身的記憶體地址,然後再用 * 解引用,解出來的就是頭指標的記憶體地址

注意一下,這裡的LinkList *L ,涉及到了C語言裡,函式引數傳遞的規則.

*L表示地址傳遞,雖然在C裡面沒有真正意義上的地址傳遞。

所以這個過程應該是,傳入L實參的地址,即傳入形參*L的值為  &L (是指標L的本身記憶體地址),也就是要操作指標L的地址的話,需要用一個二級指標來做 * 操作

另外,我理解,這個L其實是一個頭指標,他的指標域是指向第一個結點的

這部分會比較難理解,自己畫了個圖

從上面的結構圖可以看出,p=*L 就等於指標L的地址,在第一輪歷遍的時候,頭指標和p是重合的,但是在後面的歷遍中,p會不斷地向後移動

為什麼不用值傳遞!因為,值傳遞不改變實際引數,而地址傳遞是在記憶體地址上進行操作的,所以改變實際引數的!

而這裡插入元素,是需要改變原來L連結串列的結構的!!

當開始迴圈的第一遍時,p 就指向第一個結點,並且在p && j<i 的情況下,p=p->next,也就是p指標的指向,從當前位置後移一位

3 單鏈表的元素刪除

刪除和插入沒啥大的結構上區別,也即是把前驅指標和後繼指標進行調整,注意順序

/* 初始條件:順序線性表L已存在,1≤i≤ListLength(L) */
/* 操作結果:刪除L的第i個數據元素,並用e返回其值,L的長度減1 */
Status ListDelete(LinkList *L,int i,ElemType *e) 
{ 
	int j;
	LinkList p,q;
	p = *L;
	j = 1;
	while (p->next && j < i)	/* 遍歷尋找第i個元素 */
	{
        p = p->next;
        ++j;
	}
	if (!(p->next) || j > i) 
	    return ERROR;           /* 第i個元素不存在 */
	q = p->next;
	p->next = q->next;			/* 將q的後繼賦值給p的後繼 */
	*e = q->data;               /* 將q結點中的資料給e */
	free(q);                    /* 讓系統回收此結點,釋放記憶體 */
	return OK;
}

4 單鏈表的整表建立

連結串列的建立實際上是一個動態的過程,因為每次多加一個元素,他的記憶體空間才會擴充一次,即用即擴充套件,所以他需要用到malloc

另外新建連結串列也可以有2種方法,頭插法和尾插法

先來看頭插法

/*  隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(頭插法) */
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;						/*  插入到表頭 */
	}
}

沒啥好多說的,有一句需要著重強調

*L = (LinkList)malloc(sizeof(Node))

這句的意思是什麼?(LinkList)malloc(sizeof(Node)) 返回的是一個指標變數!!!是指向一個結點(也就是頭結點)的指標!!!

而這個等式,讓*L這個指標,被引向了指向頭結點的指標,在此刻等同於頭結點的指標

為什麼說是建立了一個頭結點,是因為(*L)->next = NULL

接著是尾插法,這個屬於一般比較正常的思路,從尾部插入

/*  隨機產生n個元素的值,建立帶表頭結點的單鏈線性表L(尾插法) */
void CreateListTail(LinkList *L, int n) 
{
	LinkList p,r;
	int i;
	srand(time(0));                      /* 初始化隨機數種子 */
	*L = (LinkList)malloc(sizeof(Node)); /* L為整個線性表 */
	r=*L;                                /* r為指向尾部的結點 */
	for (i=0; i<n; i++) 
	{
		p = (Node *)malloc(sizeof(Node)); /*  生成新結點 */
		p->data = rand()%100+1;           /*  隨機生成100以內的數字 */
		r->next=p;                        /* 將表尾終端結點的指標指向新結點 */
		r = p;                            /* 將當前的新結點定義為表尾終端結點 */
	}
	r->next = NULL;                       /* 表示當前連結串列結束 */
}


這裡又有一個比較重要的語句  r=*L

這裡 *L 是指向頭結點的指標,但是L是代表整個線性表,他會隨著n的增大而增大

r是指向結點的指標,但是他會在每次歷遍,指向不同的結點

為什麼要這樣區分呢?因為連結串列的長度的計算,是按照從*L這個頭結點開始,經過n次歷遍,直到最後一個結點的next為NULL的時候才停止。

也就是說,  *L必須停留在起始位置

而 r 在程式剛開始的時候和*L是重合的。只是在後面的歷遍過程中,逐漸分開了。

r 在歷遍過程中,始終為指向最後一個節點的指標。

兩個語句需要補充理解:

r->next = p    將r 和新的結點p進行連線

r = p ,將當前指向 r 的指標 引向 p ,也就是當前最後一個結點,等於是重新整理了 r

這裡補充一下計算連結串列長度的函式,就可以看到,為什麼*L要停留在頭部

/* 初始條件:順序線性表L已存在。操作結果:返回L中資料元素個數 */
int ListLength(LinkList L)
{
    int i=0;
    LinkList p=L->next; /* p指向第一個結點 */
    while(p)                        
    {
        i++;
        p=p->next;
    }
    return i;
}

計算過程從L開始歷遍,每次歷遍計數器加1 ,最後返回的是計數器數字

5 單鏈表的整表刪除

他關鍵也是用到指標的移動和free函式

/* 初始條件:順序線性表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;
}

從前往後依次free掉結點,組後把頭結點的next設為NULL,即置空!

這樣,兩種儲存結構的線性表,就基本是這樣,下一篇再寫點迴圈連結串列和雙向連結串列.



相關推薦

資料結構 --- 線性 順序儲存 儲存

線性表是平時一直會用到的資料結構,像python裡面的list這種高階資料結構,其實也是對這種底層結構的封裝。 這篇文章寫了整整4天........ 線性表的儲存結構主要分兩大類,一類一類來看。 在這之前,先用虛擬碼來形容一下線性表擁有的基本功能 1 :順序儲存

C++資料結構-線性順序儲存結構設計

線性表的順序儲存結構:指的是用一段地址連續的儲存單元一次儲存線性表中的資料元素,如下: 有了以上概念,我們可以使用一維陣列來實現線性表的順序儲存結構,     儲存空間:T* m_marry     當前長度:int m_length 整個類繼承於之前我們寫的List類,

《大話資料結構5》—— 佇列的儲存結構 —— C++程式碼實現

目錄 鏈佇列 迴圈佇列和鏈式佇列的比較 鏈佇列 ●  實現佇列的最好的方式就是使用單鏈表來實現,佇列的鏈式儲存結構,其實就是線性表的單鏈表,只不過它只能尾進頭出而已——稱為鏈佇列。 ● 那為了操作方便,頭指標指向頭結點,隊尾指標指向終端節點,即最後一個結點元

資料結構-線性-順序

順序表程式碼實現 定義 順序表=陣列+整形長度 包含功能 建立空白順序表 陣列轉順序表 向後新增一個元素 顯示所有元素 刪除指定元素 查詢元素 插入元素 #include <iostream> #defi

資料結構線性順序結構c語言實現程式碼

#include<stdio.h> #include<stdlib.h> typedef int ElementType; typedef struct LNode * PtrToLNode; struct LNode{ ElementType D

資料結構---線性(順序棧)

SqStack.h #ifndef __SQSTACK_H__ #define __SQSTACK_H__ //一些庫函式的標頭檔案包含 #include <string.h> #include <ctype.h> #include <malloc.h> #

資料結構學習筆記——堆疊之儲存結構(c語言實現)

棧的鏈式儲存結構使用單鏈表實現,同線性表一樣,鏈式儲存結構的堆疊在儲存空間的利用上顯得更加靈活,只要硬體允許一般不會出現溢位的情況。但鏈式棧相對於順序棧稍顯麻煩,若所需堆疊空間確定,順序棧顯得方便一些。關於鏈式和順序式的選擇視具體情況而定。 1.棧的鏈式儲存結構

資料結構——線性——順序

1. 線性表   線性表是最常用且最簡單的資料結構。簡而言之,一個線性表是n個數據元素的有限序列。至於每一個元素的具體含義,在不同的情況下各不相同,它可以是一個數字或一個符號,也可以是一頁書,甚至是更復雜的資訊。 2. 線性表的特點 存在唯一一個被稱

資料結構-------線性..順序..連結串列..順序和連結串列的區別和聯絡(1)

線性表:        線性表是N個具有相同特性的資料元素的有限序列 .線性表是一種在實際中廣泛使用的資料結構,常見的線性表:順序表..連結串列...棧...佇列...字串... 線性表在邏輯上是線性結構,也就說是連續的一條直線,但是在物

資料結構- - 線性- -順序_完善

豐富了前邊的功能,更加完善。 #include <iostream> #include <stdlib.h> #define LIST_INIT_SIZE 100 //線性表儲存空間的初始分配量 #define LISTINCREMENT 10 //線性表儲存空間的分配增

資料結構- -線性- -順序

線性表屬於線性結構,是最常用且最簡單的一種資料結構,是有n個數據元素的有限序列,同一線性表中的元素必定具有相同特性。長度可根據需要增長或縮短,即對線性表的資料元素可以進行訪問,還可以進行插入和刪除等。 順序儲存與實現: 把邏輯上相鄰的資料元素儲存在物理也相鄰的儲存單元中。用一組地址連續的儲存

資料結構 一元稀疏多項式 利用儲存實現儲存一元多項式,並計算兩個一元多項式之和

一、實驗原理 利用鏈式儲存實現儲存一元多項式,並計算兩個一元多項式之和。一元多項式由係數和指數構成。 1、create()儲存係數指數:首先建立一個頭結點headLnode,從headLnode->next開始存入係數和指數,只有係數是0時,才表示這個多項式的結束,否

大話資料結構九:佇列的儲存結構佇列)

1. 鏈佇列的特點: 鏈佇列其實就是單鏈表,只不過它是先進先出的單鏈表,為了實現方便,程式中設定了隊頭(front),隊尾(rear)兩個指標。 2. Java使用連結串列實現佇列:  //結點類,包含結點的資料和指向下一個節點的引用 public class N

資料結構學習筆記-棧的儲存(C語言實現)

棧是一個特殊的線性表,後進先出,既然是線性表,就會分為順序儲存和鏈式儲存,下面就是棧的鏈式儲存部分,也稱作鏈棧。單鏈表是有頭指標頭節點的,通常鏈棧的棧頂就相當於頭指標,因為初始化的鏈棧是空的,即top=

資料結構學習筆記-佇列的儲存(C語言實現)

佇列是一種先進先出的線性表。是線性表就會有鏈式儲存,因為先進先出,鏈佇列就是在鏈尾進鏈頭出,這樣一來,定義鏈佇列時就需要定義兩個指標,分別指向佇列的隊頭(相當於頭指標)、隊尾。如果隊頭等於隊尾,則該鏈佇

資料結構》嚴蔚敏 儲存實現佇列 演算法3_4_2

這個用連結串列代替陣列來實現佇列 這個程式碼書寫的時候注意ClearQueue(Link_Queue *q) 和DestroyQueue(Link_Queue *q)這兩個函式 前者在寫的時候:要注意第一步是先將rear指標挪到指向佇列首元素的位置,然後一舉將剩下的元素一併釋放,接著再釋

資料結構|二叉樹的儲存(實驗6.2)

一、實驗目的 1、   熟練理解樹和二叉樹的相關概念,掌握的儲存結構和相關操作實現; 2、   掌握樹的順序結構的實現; 3、   學會運用樹的知識解決實際問題 二、 實驗內容 1、自己確定一個二叉樹(樹結點型別、數目和結構自定)利用鏈式儲存結構方法儲存。實現樹

常用資料結構-二叉樹的儲存、建立和遍歷

1. 鏈式二叉樹簡介         二叉樹是資料結構——樹的一種,一般定義的樹是指有一個根節點,由此根節點向下分出數個分支結點,以此類推以至產生一堆結點。樹是一個具有代表性的非線性資料結構,所謂的非

資料結構 線性中,順序儲存儲存的優缺點

簡單對順序儲存和鏈式儲存結構做對比:   儲存分配方式; 順序儲存用一段連續的儲存單元一次儲存線性表的資料元素。                          鏈式儲存採用鏈式儲存結構,用一組任意的儲存單元存放線性表的元素。時間複雜度衡量;               

資料結構線性儲存結構單鏈(C++)

一. 標頭檔案—linkedlist.h 1 #ifndef _LIKEDLIST_H_ 2 #define _LIKEDLIST_H_ 3 4 #include <iostream> 5 6 template <class T> 7 struc