大話數據結構學習之(二) 線性表

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


用一段連續的存儲單元一次存儲線性表的數據元素。


結構代碼:

const int MAXSIZE = 20; // 存儲空間初始分配量
typedef int ElemType; // ElemType類型根據實際情況而定,這裏假設爲int
typedef struct
{
	ElemType data[MAXSIZE]; // 數組存儲數據元素
	int length; // 線性表當前長度
}SqList;

這裏,我們發現描述順序存儲結構需要三個屬性:

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

1.1 數組長度與線性表長度的區別:

數組長度是存放線性表存儲空間的長度,線性表長度是線性表中數據元素的個數。任意時刻線性表的長度應小於等於數組長度。


其存取性能爲O(1),通常把具有這一特點的存儲結構成爲隨機存儲結構

1.2 順序存儲結構的插入與刪除

1.2.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不在範圍內時,注意是length+1
		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;
}

1.2.2 刪除操作:

// 初始條件:順序線性表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) // 當i不在範圍內
		return ERROR;
	if (i<L->length) // 當刪除位置不在表尾
	{
		for (k=i-1; k<L->length; k++) // 將要刪除位置後數據元素前移一位
		{
			L->data[k] = L->data[k+1];
		}
	}
	L->length--;
	return Ok;
}

複雜度
最好情況O(1),最差情況O(n),平均複雜度爲O((n-1)/2) = O(n)。
這說明,它比較適合於元素個數不太變化,而更多是存取數據的應用。

1.3 線性表順序存儲結構的優缺點:

優點:
  • 無需爲表中元素之間的邏輯關係而增加額外的存儲空間
  • 可以快速存取表中任意位置的元素
缺點:
  • 插入刪除操作需要移動大量元素
  • 當線性表長度變化較大時,難以確定存儲空間容量
  • 造成存儲空間“碎片”


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


鏈表中第一個結點的存儲位置叫做頭指針。線性鏈表的最後一個結點指針爲“空”(通常用NULL或“^”符號表示)。
有時,我們爲了更加方便的對鏈表進行操作,會在單鏈表的第一個結點前附設一個結點,稱爲頭結點。頭結點的數據域可以不存儲任何信息,也可以存儲如線性表長度等附加信息,頭結點的指針域存儲指向第一個結點的指針。



頭結點與頭指針的異同:

頭指針:
  • 頭指針是指鏈表指向第一個結點的指針,若鏈表有頭結點,則是指向頭結點的指針
  • 頭指針具有標識作用,所以常用頭指針冠以鏈表的名字
  • 無論鏈表是否爲空,頭指針均不爲空。頭指針是鏈表的必要元素
頭結點:
  • 頭結點是爲了操作的統一和方便而設立的,放在第一個元素的結點之前,其數據域一般沒有意義(也可以放鏈表長度)
  • 有了頭結點,對在第一個結點前插入和刪除第一個結點,其操作與其他結點的操作就統一了
  • 頭結點不一定是鏈表的必要元素

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


2.1 單鏈表的讀取


// 初始條件:順序線性表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 = p->next;
		++j;
	}
	if (!p || j>i)
	{
		return ERROR; // 第i個元素不存在
	}
	*e = p->data; // d取第i個元素的數據
	return Ok;
}

說白了,就是從頭開始找,直到第i個元素爲止。時間複雜度取決於i的位置,i=1時不需要遍歷,而i=n時,則遍歷n-1次纔可以。


2.2 單鏈表的插入與刪除


2.2.1 單鏈表的插入


// 初始條件:順序線性表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;
	j = 1;
	while(p && j<i)
	{
		p = p->next;
		++j;
	}
	if (!p || j>i)
	{
		return ERROR; // 第i個元素不存在
	}
	s = (LinkList)malloc(sizeof(Node));// 生成新結點
	s->data = e;
	s->next = p->next;// 將p的後繼結點賦值給s的後繼
	p->next = s; // 將s賦值給p的後繼
	return Ok;
}
對於單鏈表的表頭和表尾的特殊情況,操作是相同的。

2.2.2 單鏈表的刪除


// 初始條件:順序線性表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)
	{
		p = p->next;
		++j;
	}
	if (!(p->next) || j>i)
	{
		return ERROR; // 第i個元素不存在
	}
	q = p->next;
	p->next = q->next; 
	*e = q->data; // 將q結點中的數據給e
	free(q); // 回收此結點
	return Ok;
}


對插入刪除數據越頻繁的操作,單鏈表的效率優勢就越明顯。


2.3 單鏈表的整表創建


創建單鏈表的過程就是一個動態生成鏈表的過程,即從“空表”的初始狀態起,依次建立各個元素結點,並逐個插入鏈表。

2.3.1 頭插法



// 隨機產生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;// 插入到表頭
	}
}


2.3.2 尾插法


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


2.4 單鏈表的整表刪除


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

要知道p是一個結點,除了數據域還有指針域,free(p)是對整個結點進行刪除和內存釋放工作。所以如果程序中直接像下面這樣寫會出問題的(p的地址域已經被釋放)
free(p);
p = p->next;

2.5 單鏈表結構與順序存儲結構的比較


  • 頻繁查找,很少進行插入刪除操作,宜採用順序存儲結構;反之用單鏈表。比如遊戲開發中,對於用戶註冊的個人信息,除了註冊時插入數據外,絕大多數情況都是讀取,所以應考慮用順序存儲結構。而遊戲中玩家的武器裝備列表,隨着玩家的遊戲過程中,可能隨時增加或刪除,單鏈表就可以大展拳腳了。
  • 當線性表中元素個數變化較大或者根本不知道有多大時,最好用單鏈表結構,這樣就不需要考慮存儲空間大小問題。若事先知道大致長度,如一年12個月,這種用順序結構效率會高很多。

2.6 循環鏈表


對於非空的循環鏈表如下:

其實循環鏈表和單鏈表的主要差異就在於循環的判斷條件上,原來是判斷p->next是否爲空,現在是它不等於頭結點,則循環結束。

2.7 雙向鏈表



雙向鏈表是在單鏈表的每個結點中,再設置一個指向其前驅結點的指針域。

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

對於雙向鏈表中的一個結點p,它的後繼的前驅,以及前驅的後繼都是自己。

2.7.1 插入操作


插入操作時,並不複雜,但是順序很重要,不能寫反了。




s->prior = p; // 把p賦值給s的前驅,如圖中的1
s->next = p->next; // 把p->next賦值給s的後繼,如圖中2
p->next->prior = s; // 把s賦值給p->next的前驅,如圖中3
p->next = s;       // 把s賦值給p的後繼,如圖中的4

2.7.2 刪除操作





p->prior->next = p->next; 
p->next->prior = p->prior;
free(p);




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