m數據結構 day2 線性表List(一)數組,單鏈表

定義

最簡單的最常用的數據結構,以排隊的方式去組織數據

線性,就是像線一樣,前面是誰,後面是誰,串聯好的次序

一對一:每個數據前面只能有一個數據,後面也只能有一個數據。前面沒數據的就是起點,後面沒數據的就是終點。

官方概念:線性表是0個或多個數據元素的有限序列。(0個元素則叫做空表)

線性表的一個數據元素可以有多個數據項。

操作

這裏只寫了一些最基本的使用最普遍的,實際上不同的應用中,需要線性表的操作可能多種多樣,但是一般都可以用下面這些基本操作的組合來實現。
比如A和B兩個線性表的並就是把A和B兩個表的元素都插入到一個新的線性表中,但是重複項只插入一次

  • 創建,初始化:建立一個空的線性表
  • 重置爲空表
  • 查找:根據數據的位序找到數據元素,就像數組根據索引找值
  • 查找某個元素是否存在
  • 獲得線性表的長度
  • 插入一個數據
  • 刪除一個數據
//操作,沒寫返回值,其中參數爲指針的都可以改爲引用或者按值傳遞
InitList(*L);//參數是指向List的指針
IsListEmpty(*L);//如果爲空,返回true
ClearList(*L);//清空重置
GetElem(*L, index, *e);//把線性表中index處的內容給e指向的位置
LocateElem(*L, e);//查找線性表中有無元素e,如果有,返回位置(索引),否則返回0
ListInsert(*L, index, e);//在index處插入元素e
ListDelete(*L, index, *e);//刪除線性表的index處的元素,放入e指向位置
ListLength(*L);//返回元素個數,即線性表長度

假設把線性表A和B的union放入A:

void ListUnion(*La, *Lb)
{
	unsigned int len_a = ListLength(*La);
	unsigned int len_b = ListLength(*Lb);
	ElemType e;
	int i;
	for (i = 0; i < len_b; ++i)
	{
		GetElem(*Lb, i, *e);//這裏只是僞代碼,注意實際中不能這樣,沒初始化就解引用
		if (!LocateElem(*La, e))
		{
			ListInsert(*La, ++len_a, e);
		}
	}
}

兩種物理結構

在這裏插入圖片描述

順序存儲結構

在這裏插入圖片描述

關鍵詞:地址連續,每個數據元素的數據類型都一樣

用一維數組實現順序存儲結構的線性表

描述順序存儲結構需要三個屬性:

  • 起始位置
  • 線性表最大存儲容量
  • 線性表當前長度(小於等於最大長度)

可以用C語言實現,或者其他語言:

typedef int Type;//增加代碼的通用性
const int MAXSIZE = 10;//最大容量
typedef struct
{
	Type data[MAXSIZE];//data數組名,就是起始位置
	int length;//當前長度
}SqList;

隨機存取結構:存取時間性能是 0(1)的存儲結構

由於一維數組這種線性表的起始位置知道,地址又連續,所以其中任意位置的地址可以被很簡單地立即算出,用時間複雜度的概念來說就是:存取時間性能是O(1)。

這種存儲結構我們就成爲隨機存儲結構。

所以順序結構的線性表是一種隨機存儲結構。

獲得元素操作(非常簡單)

對於順序結構的線性表,返回某個位置的元素非常簡單,這就是隨機訪問,因爲順序存儲線性表就是隨機存儲結構。

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;

Status GetElem(SqList L, int i, ElemType *e)
{
	if (L.length == 0 || i < 0 || i > L.length)
		return ERROR;
	*e = L.data[i];
	return OK;
}

插入操作(複雜)

在這裏插入圖片描述

Status ListInsert(SqList * L, int i, ElemType e)
{
	int k;
	if (L->length == MAXSIZE)
		return ERROR;
	if (i < 0 || i > L->length)
		return ERROR;
	if (i < L->length - 1)//插入位置不在表尾
	{
		for (k = L->length-2;k>=i;k--)
			L->data[k+1] = L->data[k];
	}
	L->data[i] = e;//插入新元素
	L->length++;
	return OK;
}

複雜度:

  • 最好情況:插入位置是線性表的末尾,不需要移動元素,所以複雜度O(1)
  • 最壞情況:插入位置是線性表的起始位置,即第一個位置,需要所有元素後移一步,所以複雜度O(n),n是線性表長度
  • 平均情況:插入到第i個位置,需要移動n-i個元素,如果i靠前,則移動的元素就多,反之反之。平均起來,i取n/2,則複雜度O(n12)O(\frac{n-1}{2}),忽略常數,即仍然是O(n).

所以,線性表的順序存儲結構在存入或者讀取數據時的複雜度是O(1);在插入或者刪除數據的複雜度是O(n).所以他很適合不經常插入和刪除,而是經常讀取和存入(不是插入)的應用場景。

刪除操作

  • 隊列長度如果爲0,拋出異常
  • 再看索引範圍是否正確,刪除位置不合理則拋出異常
  • 取出刪除元素
  • 從索引i+1開始,一直到length-1的元素全部往前移動一位
  • 長度減1
Status DeleteList(SqList * L, int i, ElemType & e)
{
	int k;
	if (L->length == 0)
		return ERROR;
	if (i < 0 || i > L->length-1)
		return ERROR;
	e = L->data[i];//取出元素,這裏由於要保證函數執行完畢後e的值不丟失,使用引用類型
	for (k = i + 1; k < length; ++k)
	{
		L->data[k-1] = L->data[k];
 	}
 	L->length--;
 	return OK;
}

刪除操作的算法複雜度和插入一毛一樣。

優缺點

在這裏插入圖片描述
優點第一點:比如鏈表就需要除了數據項以外,還要給一個指針分配空間,用於指向下一個數據項的位置。

鏈式存儲結構:“亂”中有“序”

如圖,物理內存位置亂七八糟,不像順序結構那樣緊緊挨着排成一列,非常集中,可以相鄰也可以不相鄰,是一種動態的零散的結構;但是邏輯位置卻又十分有序,是一個線性的單鏈。我覺得亂中有序來形容鏈表,雙重意味,最貼切不過。

在這裏插入圖片描述

但是鏈式結構需要額外空間存儲下一個元素的地址,不僅要處處數據項。存數據的域叫做數據域,存後繼元素地址的域叫做指針域。這兩部分組合在一起,成爲一個數據元素的存儲映像,被稱爲結點Node.
在這裏插入圖片描述

單鏈表:結點只有一個指針域

如果每個結點只有一個指針域,就叫做單鏈表。因爲這唯一的一個指針域中存儲的是指向後繼結點的指針next,只能訪問後繼結點,所以是單向的。

頭指針:指向第一個元素,不可能爲空指針

第一個結點的內存位置叫做頭指針,即指向第一個元素

就算鏈表是空的,頭指針也不可能是空的,因爲頭指針一定會指向第一個結點,如果是空鏈表,也會有第一個結點,只不過第一個結點的數據域不存東西,指針域存空指針(這就表示這個結點也是最後一個結點)。

他是鏈表的必需元素,不能沒有

但是如果鏈表有頭結點,則頭指針是頭結點指針域中存儲的那個指針

尾指針:NULL,空指針

在這裏插入圖片描述

頭結點(可以沒有但一般都會用,因爲它可以讓空鏈表和非空鏈表的處理一致)

有時候會專門給鏈表附加一個頭結點,不是爲了存儲數據,只是爲了操作方便和統一設立的,數據域可以不存東西,也可以存整個鏈表的長度信息。

  1. 方便: 可以直接獲取到鏈表長度,無需遍歷一次
  2. 統一:有了頭結點,則對第一個元素前面插入元素,以及刪除第一個元素的操作,就和其他節點一致了;且頭結點使得所有鏈表(即使是空鏈表)的頭指針都不爲空指針(空鏈表的頭結點的指針域中的指針爲空指針)。這種統一性是頭結點帶來的最大的好處。

當有頭結點的時候,頭指針指向頭結點,頭結點指針域的指針指向第一個常規結點

沒有頭結點的時候,頭指針指向第一個常規結點(存儲數據)

頭指針並不是頭結點的指針域中的內容!

  • 沒有頭結點的單鏈表(頭指針指向第一個常規結點,尾結點指針域的符號表示空指針)
    在這裏插入圖片描述
  • 有頭結點的單鏈表(頭指針指向頭結點,頭結點指針域的指針指向第一個常規結點)
    頭結點的數據域是黑色代表沒有數據項,但是可以存其他附加信息

在這裏插入圖片描述

  • 有頭結點的空鏈表(頭指針指向頭結點,頭結點指針域的指針指向第一個常規結點)

這時候頭結點的指針域存儲空指針

所以頭結點的好處是:使得空鏈表中頭指針也不爲空指針!!空鏈表的頭結點的指針域中的指針爲空指針
在這裏插入圖片描述

用C的結構實現結點

typedef struct
{
	ElemType data;//數據域
	Struct Node * next;//指針域
}Node, *LinkList;

//typedef struct Node * LinkList;

如果p是一個指針,且p->data = aia_i,則p->next->data= ai+1a_{i+1}

獲取元素操作:遍歷,指針右移

只能遍歷,一個一個找。核心就是利用指針的右移一個一個地遍歷,其實很多算法都要用到這種技術。
在這裏插入圖片描述

Status GetElem(LinkList L, int i, ElemType & e)
{
	LinkList p = L->next;//L是頭指針,指向頭結點,p被初始化爲第一個結點的地址,即指向第一個結點
	int j = 0;
	while (p && j < i)
	{
		p = p->next;
		++j;
	}
	if (j == i)
	{
		e = p->data;
		return OK;
	}
	return ERROR;//無此數據
}

算法時間複雜度取決於i的位置:
最好情況,i=0,則立刻就找到了,不需要遍歷
最壞情況,i=n-1,n爲鏈表長度,則需要遍歷n-1次,最壞情況時間複雜度是O(n)

插入和刪除

都是先遍歷找到索引爲i的元素,然後插入或者刪除。時間複雜度也和i的位置有關,i=0是最好情況,無需遍歷,最壞情況i=n-1,光遍歷的時間複雜度就是O(n),但是插入和刪除的核心操作都只是一句代碼的事,時間複雜度是O(1)。

所以插入和刪除的時間複雜度總的來說都是O(n),主要時間都花在遍歷上了。

插入操作

在這裏插入圖片描述

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

在這裏插入圖片描述
在這裏插入圖片描述

在這裏插入圖片描述

Status InsertItem(LinkList L, int i, ElemType e)
{
	LinkList s;
	LinkList p = L->next;//L指向頭結點,p指向第一個結點
	int j = 0;
	while (p && j < i)
	{
		p = p->next;
		++j;
    }
    if (!p)
    	return ERROR;
    LinkList s = new LinkList;//C++可以在任何位置聲明變量
    //C形式,要在函數最前面聲明s
    //s = (LinkList)malloc(sizeof(Node));
    
    s->data = e;
    s->next = p->next;//p現在指向第i-1個元素
    p->next = s;
    return OK;
}

刪除操作

在這裏插入圖片描述

Status DeleteItem(LinkList L, int i, ElemType * e)
{
	//L是頭結點中存的指針,指向第一個常規結點
	LinkList p = L->next;
	int j = 0;
	while (p->next && j < i)
	{
		p = p->next;
		++j;
	}
	if (!(p->next))
		return ERROR;
	LinkList q = p->next;//必須用q存住p->next以釋放它
	*e = p->next->data;
	p->next = p->next->next;//核心代碼就這一句
	free(q);
	return OK;
}

單鏈表的動態創建

順序結構的線性表的創建,實際上就是初始化一個數組,只需要告訴數組元素的數據類型和數組長度;

但是鏈表是動態結構,事先不知道鏈表長度,所以不能事先分配,只能根據需求即時生成一個結點並插入到鏈表中。

頭插法:把新結點插在頭結點後面,做第一個常規結點

每次把新結點插在頭結點後面。做第一個常規結點。

void CreateListHead(LinkList *L, int n)
{
	//L是指向頭指針(*L)的指針,因爲LinkList 是struct Node *
	//n是要創建的項數
	LinkList p;
	int i;
	srand(time(0));//設置隨機數種子
	//先創建空鏈表:只有頭結點,且頭結點的指針域中存空指針
	*L = (LinkList)malloc(sizeof(Node));//*L是頭指針,指向剛分配的結點:頭結點
	(*L)->next = NULL;//空鏈表創建成功

	for (i = 0; i < n; ++i)
	{
		//生成新節點
		p = (LinkList)malloc(sizeof(Node));
		p->data = rand() %100 + 1;
		//把新節點p插入到第一個位置
		p->next = (*L)->next;
		(*L)->next = p;
	}
}

徹底被這幾個指針搞暈了,拿了紙筆一通分析,得到以下結論:

  • L指向頭指針的指針,頭指針指向頭結點,頭結點的指針域中存的指針指向第一個常規結點!LLinkList *類型

  • *L是頭指針,指向頭結點,而頭結點的指針域中存的那個指針指向第一個結點(非頭結點,第一個存內容的常規結點)。*L,解引用一下,是LinkList類型,即struct Node *類型。

  • **L是頭結點。對*L再解引用一下,得到Node類型。

我畫了個草圖:
在這裏插入圖片描述

尾插法

每次把新結點插在前一個新結點後面,即終端節點的後面,先來後到。

這裏要用一個指向尾結點的指針rear

void CreateListTail(LinkList * L, int n)
{
	LinkList p, r;
	int i;
	srand(time(0));
	//先創建空鏈表
	*L = (LinkList)malloc(sizeof(Node));
	(*L)->next = NULL;
	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;
}

在這裏插入圖片描述

單鏈表的整表刪除:在內存中釋放掉它佔用的空間

由於鏈表的每一個結點都是動態創建的,是動態分配地堆內存,所以刪除鏈表必須要釋放每一個結點的地址,否則就會有內存泄漏的危險。

考慮挨個釋放,先把第一個free,問題來了,第二個結點在哪兒都不知道了······因爲第二個結點的位置在第一個結點裏。所以:

  1. 聲明新結點p和q
  2. 把第一個結點賦給p
  3. 循環:
  • 把下一結點給q
  • 釋放p
  • 把q賦給p
Status ClearList(LinkList *L)
{
	//*L是頭指針
	LinkList p, q;
	p = (*L)->next;//p指向第一個結點
	while (p)
	{
		q = p->next;
		free(p);
		p = q;
	}
	//至此p爲空指針
	(*L)->next = NULL;//頭結點的指針域存儲空指針,表示空鏈表
	return OK;
}

順序結構和單鏈表結構的對比

在這裏插入圖片描述

說白了就是單鏈表更靈活。

順序結構的唯一優點:查找快。如果非要再加一個,那就是代碼不好寫點。

單鏈表的唯一缺點:查找慢。如果非要再加一個,那就是代碼好寫點。

這麼看的話,我還是乖乖用鏈表吧。

當然,我上面兩句總結有些偏頗,不能說鏈表就一定比順序結構的數組好,要看應用場景,有時候還真是數組合適一些。

在這裏插入圖片描述

在這裏插入圖片描述

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