大話數據結構筆記——第三章:線性表

線性表的定義

線性表(List):零個或多個數據元素的有限序列,直接前驅元素直接後繼元素。在線性表中間的數據元素,有且只有一個直接前驅元素和直接後繼元素。線性表元素的個數n(n>=0)定義爲線性表的長度,當n=0時稱爲空表。在較複雜的線性表中,一個數據元素可以由若干個數據項組成。

線性表的抽象數據類型

ADT 線性表

DATA Operation
線性表的數據對象集合爲{a1a_1,a2a_2,……,ana_n},數據類型DataType。其中,除了第一個元素外,每個元素有且只有一個前驅元素,除了最後一個元素外,沒有元素有且只有一個後繼元素。數據元素之間的關係是一對一的關係 InitList(*L):初始話操作,建立一個空的線性表 ,ListEmpty(L),ClearList(*L),GetElem(L,i,*e)

end ADT
Tip:感覺像面向對象編程一樣

線性表的順序存儲結構

指的是用一段地址連續的存儲單元一次存儲線性表的數據元素。所以可以用一維數組來實現順序存儲結構。順序存儲結構的三個屬性:

  1. 存儲空間的起始位置:數組data,它的存儲位置就是存儲空間的存儲位置。
  2. 線性表的最大存儲容量:數組長度MaxSize。
  3. 線性表的當前長度:length。

數組長度與線性表長度的區別:
數組長度一般不變,線性表長度會隨着數據的插入和刪除操作而產生變化,不過,在任意時刻,線性表的長度應該小於等於數組的長度。

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

獲取元素操作

#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 < 1 || i > L.length)
		return ERROR;
	*e = L.data[i-1];
	return OK;
	
}

插入操作

插入算法的思路:

  1. 如果插入位置不合理,拋出異常;
  2. 如果線性表長度大於等於數組的長度,則拋出異常或動態增加容量;
  3. 從最後一個元素開始向前遍歷到第i個位置,分別將它們都向後移動一個位置;
  4. 將要插入元素填入位置i處;
  5. 表長加1。
/*初始條件:順序線性表L已存在,1 <=i <=ListLength(L)*/
/*操作結果:在L中第i個位置之前插入新的數據元素e,表長+1*/

Status ListInsert(SqList *L, int i,ElemType e)
{
	int k;
	if (L->length==MAXSIZE) //線性表滿
	{
		return ERROR;
	}
	if (i<1 || i> L->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. 取出刪除元素;
  3. 從刪除元素位置開始遍歷到最後一個元素位置,分別將它們都向前移動一個位置;
  4. 表長減1。
    實現代碼:
/*初始條件:順序線性表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];
	if (i<length-1)
	{
		for (k=i; k<L->length; k++)
		{
			L->data[k-1] = L->data[k]; 
		}
	}
	L->length--; //表長減1
	return OK;
}

線性表的複雜度與優缺點

在存、讀數據時,不管在哪個位置,時間複雜度爲O(1),而在插入或刪除時,時間複雜度爲O(n)。
優點:

  1. 無須爲表示表中元素之間的邏輯關係而增加額外的存儲空間
  2. 可以快速地存取表中任一位置的元素。

缺點:

  1. 插入和刪除操作需要移動大量元素。
  2. 當線性表長度變化較大時,難以確定存儲空間的容量。
  3. 造成存儲空間的“碎片”。

線性表的鏈式存儲結構

線性錶鏈式存儲結構定義

爲了表示每個數據元素aia_i與其直接後繼元素ai+1之間的邏輯關係,對於數據元素aia_i來說,除了存儲其本身的信息之外,還需存儲一個指示其直接後繼的信息(即直接後繼的存儲位置)。把存儲數據元素信息的域稱爲數據域,把存儲直接後繼位置的域稱爲指針域。指針域中存儲的信息稱爲指針或鏈。這兩部分信息組成數據元素aia_i的存儲映像,稱爲結點(Node)
n個結點鏈結成一個鏈表,即爲線性表(a1a_1a2a_2a3a_3a4a_4……ana_n)的鏈式存儲結構,因此此鏈表的每個結點只包含一個指針域,所以叫做單鏈表
把鏈表中第一個結點的存儲位置叫做頭指針,最後一個結點指針爲NULL。有時會在單鏈表的第一個結點前附設一個結點,稱爲頭節點

頭指針和頭節點區別

頭指針:

  1. 頭指針是指鏈表指向第一個結點的指針,若鏈表有頭結點,則是指向頭結點的指針;
  2. 頭指針具有標識作用,所以常用頭指針冠以鏈表的名字;
  3. 無論鏈表是否爲空,頭指針均不爲空。頭指針是鏈表的必要元素。

頭結點:

  1. 頭結點是爲了操作的統一和方便而設立的,放在第一個元素的結點之前,其數據域一般無意義(也可以存放鏈表的長度) ;
  2. 有了頭節點,對在第一個元素結點前插入結點和刪除第一結點,其操作與其他結點的操作就統一了;
  3. 頭節點不一定是鏈表必須元素。

線性錶鏈式存儲結構代碼描述

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

假設p是指向線性表第i個元素的指針,則結點aia_i的數據域爲p->data,指針域爲p->next,p->next的值是一個指針,它指向第i+1個元素,即指向ai+1的指針,所以p->next->data=ai+1

單鏈表的獲取

算法思路:

  1. 聲明一個指針p指向鏈表的第一個結點,初始化j從1開始;
  2. 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1;
  3. 若到鏈表末尾p爲空,則說明第i個結點不存在;
  4. 否則查找成功,返回結點p的數據。
/* 初始條件:順序線性表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;
}

單鏈表的插入與刪除

單鏈表的插入

算法思路:

  1. 聲明一指針p指向鏈表頭結點,初始化j從1開始;
  2. 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1;
  3. 若到鏈表末尾p爲空,則說明第i個結點不存在;
  4. 否則查找成功,在系統中生成一個空結點s;
  5. 將數據元素e賦值給s->data;
  6. 單鏈表的插入標準語句s->next = p->next;p->next=s;
  7. 返回成功。

實現代碼:

/* 初始條件:順序線性表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) //尋找第i-1個結點
	{
		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;
}

單鏈表的刪除

算法思路:

  1. 聲明一指針p指向鏈表頭結點,初始化j從1開始
  2. . 當j<i時,就遍歷鏈表,讓p的指針向後移動,不斷指向下一結點,j累加1;
  3. 若到鏈表末尾p爲空,則說明第i個結點不存在;
  4. 否則查找成功,將欲刪除的結點p->next賦值給q;
  5. 單鏈表的刪除標準語句p->next=q->next;
  6. 將q結點中的數據賦值給e,作爲返回;
  7. 釋放q結點;
  8. 返回成功。

實現代碼:

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

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

單鏈表的整表創建

創建單鏈表的過程就是動態生成鏈表的過程,即從“空表”的初始狀態起,依次建立各元素結點。
算法思路:

  1. 聲明一指針p和計數器變量i;
  2. 初始化一空鏈表L;
  3. 讓L的頭結點的指針指向NULL,即建立一個帶頭結點的單鏈表;
  4. 循環:
    生成一新結點賦值給p;
    隨機生成一數字賦值給p的數據域p->data;
    將p插入到頭結點與前一新結點之間。

實現代碼算法:

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

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

單鏈表的整表刪除

算法思路:

  1. 聲明一結點p和q;
  2. 將第一個結點賦值給p;
  3. 循環:
    將下一個結點賦值給q;
    釋放p;
    將q賦值給p。

實現代碼:

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

單鏈表結構與順序儲存結構優缺點

存儲分配方式:

  1. 順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素
  2. 單鏈表採用鏈式存儲結構,用一組任意的存儲單元存放線性表的元素

時間性能:

  1. 查找
    順序存儲結構O(1)
    單鏈表O(n)
  2. 插入和刪除
    順序存儲結構需要平均移動表長一般的元素,時間爲O(n)
    單鏈表在線出某位置的指針後,插入和刪除時間僅爲O(1)
  3. 空間性能
    順序存儲結構需要預分配存儲空間,分大了,浪費,分小了易發生上溢.
    單鏈表不需要分配存儲空間,只要有就可以分配,元素個數也不受限制。

所以我們要根據實際需要選擇合適的結構。不能一概而論。

靜態鏈表

用數組描述的鏈表叫做靜態鏈表,讓數組的元素都是由兩個數據域組成,data和cur,數組的每個下標都對應着一個data和cur。

/*線性表的靜態鏈表存儲結構*/
#define MAXSIZE 1000  // 假設鏈表的最大長度是1000
typedef struct 
{
	ElemType data;
	int cur; //遊標(Cursor),爲0時表示無指向

}Component,StaticLinkList[MAXSIZE];

/*將一維數組space中各分量鏈成一備用鏈表*/
Status InitList(StaticLinkList space)
{
	int i;
	for (i = 0; i < MAXSIZE-1; i++)
	{
		space[i].cur = i+1
	}
	space[MAXSIZE-1].cur = 0;
	return OK;
}

插入操作

/*在L中第i個元素之前插入新的數據元素e */
Status ListInsert(StaticLinkList L,int i, ElemType e)
{
	int j, k, l;
	k = MAX_SIZE-1; //k首先是最後一個元素的下標
	if (i < 1 || i > ListLength(L) +1;
		return ERROR;
	j = Malloc_SSL(L); //獲得空閒分量的下標
	if(j)
	{
		L[j].data = e; //將數據賦值給此分量的data
		for (l = 1; l <= i-1; l++)
		{
			k=L[k].cur;
		}
		L[j].cur = L[k].cur;//把第i個元素之前的cur賦值給新元素的cur
		L[k].cur = j; //將新元素的下標賦值給第i個元素之前的元素cur
		return OK
	}
	return ERROR;
}

刪除操作

/*刪除在L中第i個數據元素e*/
Status ListDelete(StaticLinkList L,int i)
{
	int j, k;
	if (i <1 || i >ListLength(L))
	{
		return ERROR;
	}
	k = MAXSIZE-1;
	for (j = 1; j <= i-1; j++)
	{
		k = L[k].cur;
	}
	j = L[k].cur;
	L[k].cur = L[j].cur;
	Free_SSL(L,j);
	return OK
}

/*將下標爲K的空閒結點回收到備用鏈表*/
void Free_SSL(StaticLinkList space, int k)
{
	space[k].cur = space[0].cur; //把第一個元素cur值賦給要刪除的分量cur
	space[0].cur = k;    		 //把要刪除的分量下標賦值給第一個元素的cur
}

靜態鏈表優缺點

優點:在刪除插入時,只需修改遊標,解決了在順序存儲結構中需要大量移動數據元素的問題。
缺點:沒有解決連續存儲帶來的表長難以確定的問題。
靜態鏈表是爲了給沒有指針的高級語言設計的一種實現單鏈表能力的方法。這種思考方式非常巧妙,應該理解,以備不時之需。

循環鏈表

將單鏈表中終端結點的指針端由空指針改爲指向頭結點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱爲單循環鏈表,簡稱循環鏈表

雙向鏈表

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

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