【圖解數據結構】 線性表


1.線性表的定義

若將線性表記為(a1,...,ai-1,ai,ai+1,...,an),則表中ai-1領先於ai,ai領先於ai+1,稱ai-1是ai的直接前驅元素,ai+1是ai的直接后繼元素。
線性表元素的個數n(n>=0)定義為線性表的長度,當n=0時,稱為空表。
mark

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

線性表的順序存儲結構,指的是一段地址連續的存儲單元依次存儲線性表的數據元素。

線性表的順序存儲結構如圖所示:

mark

2.1地址計算方法

用數組存儲順序表意味着要分配固定長度的數組空間,分配的數組空間大於等於當前線性表的長度,數據元素的序號和存放它的數組下標之間存在對應關系:

mark

存儲器的每個存儲單元都有自己的編號,這個編號稱為地址。

每個數據元素都需要占用一定的存儲單元空間的,假設占用的是c個存儲單元,對於第i個數據元素ai存儲位置為(LOC表示獲得存儲位置的函數):

LOC(ai) = LOC(a1) + (i-1)*c

mark

2.2線性表順序存儲的結構代碼:

#define MAXSIZE 20 /*存儲空間初始分配量*/
typedef	int ElemType;

typedef struct 
{
	ElemType data[MAXSIZE];	/*數組存儲數據元素*/
	int length;				/*線性表當前長度*/
}SqList;

描述線性表順序存儲的三個屬性:

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

2.3獲得元素操作

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef	int Status;
/*用e返回L中第i個數據元素的值*/
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;
}

2.4插入操作

思路

  • 如果線性表長度大於等於數組長度,拋出異常

  • 如果插入位置不合理,拋出異常

  • 從最后一個元素開始向前遍歷到第i個位置,分別將它們都向后移動一個位置

  • 將要插入元素填入位置i

  • 表長加1

/*在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--)
		{
			L->data[k + 1] = L->data[k];
		}
	}
	L->data[i - 1] = e;
	L->length++;
	return OK;
}

插入前

mark

插入后

mark

2.5刪除操作

思路

  • 如果為空表,拋出異常
  • 如果刪除位置不合理,拋出異常
  • 從刪除元素位置開始遍歷到最后一個元素位置,分別將它們向前移動一個位置
  • 表長減1
/*刪除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 < L->length)
	{
		for (k = i; k <= L->length; k++)
		{
			L->data[k - 1] = L->data[k];
		}
	}
	L->length--;
	return OK;
}

刪除前

mark

刪除后

mark

2.6優缺點

線性表的順序存儲結構,在存、讀數據時,不管是哪個位置,時間復雜度都是O(1);而插入或刪除時,時間復雜度都是O(n)。

優點:

  • 無需為表示線性表中的邏輯關系而增加額外的存儲空間
  • 可以快速的存取線性表中任一位置的元素

缺點:

  • 插入和刪除操作需要移動大量的元素

  • 難以確定線性表存儲空間的容量

  • 造成存儲空間的“碎片”,浪費存儲空間

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

為了每個數據元素ai與其后繼數據元素ai+1之間的邏輯關系,對數據元素ai來說,除了存儲本身的信息之外,還需要存儲一個指示其后繼元素的信息(即直接后繼元素的存儲位置)。
mark

3.1單鏈表

n個結點鏈結成一個鏈表,每個結點只包含一個指針域,叫做單鏈表。

線性鏈表中第一個結點的存儲位置叫做頭指針,整個鏈表的存取必須從頭指針開始。 線性鏈表的最后一個結點指針為“空”(通常用NULL或^表示)。

單鏈表存儲示意圖:

mark

空鏈表:

mark

3.1.1線性表鏈式存儲的結構代碼:

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

3.1.2單鏈表的讀取

在單鏈表中讀取第i個元素,我們無法一開始知道,必須從頭開始找。

讀取單鏈表中第i個數據的思路:

  1. 聲明一指針p指向單鏈表第一個節點,初始化j=1
  2. 當j<i時,就遍歷鏈表,讓p的指針向后移動,不斷的指向下一節點,j累加1
  3. 若到鏈表末尾p為空,則說明第i個節點不存在
  4. 否則查找成功,返回節點p的數據

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;

/*初始條件:順序線性表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不為空且計數器j還沒有等於i時,循環繼續*/
	{
		p = p->next;
		++j;
	}
	if (!p || j > i)
	{
		return ERROR;	 /*第i個節點不存在*/
	}
	*e = p->data;	/*取第i個節點的數據*/
	return OK;
}

動畫模擬:

mark

3.1.3單鏈表的插入

假設存儲元素e的節點為s,只需要將節點s插入到節點p和p->next之間即可。

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

也就是說讓p的后繼節點改成s的后繼節點,再把節點s變成p的后繼節點。

mark

注意:s->next = p->next;p->next = s;代碼的順序不能反。如果先p->next = s;,再s->next = p->next;,此時第一句會將p->next覆蓋成s的地址了,那么s->next = p->next;實際上就等於s->next = s;。這樣單鏈表將不再連續,插入操作就是失敗的。對於單鏈表的表頭和表尾的特殊情況,操作是相同的。

單鏈表第i個數據插入節點的思路:

  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;

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;
typedef struct Node
{
	ElemType data;
	struct Node *next;
} Node;
typedef struct Node *LinkList;

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操作結果:在L中第i個節點位置之前插入新的數據元素e,L的長度加1*/
Status ListInsert(LinkList *L, int i, ElemType e)
{
	int j;
	LinkList p = *L;
	j = 1;

	while (p && j<i)	/*尋找第i-1個節點*/
	{
		p = p->next;
		++j;
	}
	if (!p || j > i)
	{
		return ERROR;	/*第i個節點不存在*/
	}
	LinkList s = (LinkList)malloc(sizeof(Node));	/*生成新節點*/

	s->data = e;
	s->next = p->next; /*將p的后集節點賦值給s的后繼*/
	p->next = s	;	/*將s賦值給p的后繼*/
	return OK;
}

c語言的malloc標准函數,用於生成一個新的節點,實質就是在內存中分配內存用來存放節點。

測試代碼:

int main()
{
	LinkList head = (LinkList)malloc(sizeof(Node));	/*頭結點*/

	LinkList s1 = (LinkList)malloc(sizeof(Node));	/*第一個節點*/
	s1->data = 4;
	s1->next = NULL;

	head->next = s1;
	ListInsert(&head, 1, 2);	 /*第1個節點前插入2*/
	ListInsert(&head, 2, 3);	/*第2個節點前插入3*/
	ListInsert(&head, 2, 7);	/*第1個節點前插入7*/
	ListInsert(&head, 3, 5);	/*第1個節點前插入5*/
}

運行結果:

mark

動畫模擬:

mark

3.1.4單鏈表的刪除

假設存儲元素ai的節點為q,要實現從單鏈表中將節點q刪除的操作,其實是將它的前繼節點的指針指向它的后繼節點即可。

mark

q = p->next;
p->next = q->next;

單鏈表第i個數據刪除節點的算法:

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

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;
typedef struct Node
{
	ElemType data;
	struct Node *next;
} Node;
typedef struct Node *LinkList;

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操作結果:刪除L中第i個節點,並用e返回其值,L的長度減1*/
Status ListDelete(LinkList *L, int i, ElemType *e)
{
	int j;
	LinkList p = *L;
	j = 1;
	while (p->next && j<i)	/*尋找第i-1個節點*/
	{
		p = p->next;
		++j;
	}
	if (!(p->next) || j > i)
	{
		return ERROR;		/*第i個節點不存在*/
	}
	LinkList q = p->next;
	p->next = q->next;		/*將q的后繼賦值給p的后繼*/
	*e = q->data;	/*將q節點中的數據給e*/

	free(q);		/*回收此節點,釋放內存*/

	return OK;
}

c語言的free標准函數,作用是讓系統回收一個節點,釋放內存。

測試代碼:

還是使用上面插入例子的單鏈表,然后刪除單鏈表中的第3個節點:

int main()
{
	LinkList head = (LinkList)malloc(sizeof(Node));	/*頭結點*/

	LinkList s1 = (LinkList)malloc(sizeof(Node));	/*第一個節點*/
	s1->data = 4;
	s1->next = NULL;

	head->next = s1;
	ListInsert(&head, 1, 2);	 /*第1個節點前插入2*/
	ListInsert(&head, 2, 3);	/*第2個節點前插入3*/
	ListInsert(&head, 2, 7);	/*第1個節點前插入7*/
	ListInsert(&head, 3, 5);	/*第1個節點前插入5*/

	int e;
	ListDelete(&head, 3, &e);	/*刪除第3個節點*/
}

運行結果:

mark

動畫模擬:

mark

3.1.5單鏈表的整表創建

順序存儲結構的創建,其實就是一個數組的初始化;而單鏈表和順序存儲結構就不一樣,它所占用的空間的大小和位置是不需要預先分配划定的。所以創建單鏈表的過程就是一個動態生成鏈表的過程,即從“空表”的初始狀態起,依次建立各元素節點,並逐個插入鏈表。

單鏈表創建的思路:

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

頭插法

代碼實現:

/*頭插法*/
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;		/*插入到表頭*/
	}
}

測試代碼:

int main()
{
	LinkList list;
	CreateListHead(&list, 5); /*創建一個有5個節點的單鏈表(不包含頭結點)*/
}

運行結果:

mark

動畫模擬:

mark

尾插法

代碼實現:

void CreateListTail(LinkList *L, int n)
{
	LinkList p,r;
	int i;

	srand(time(0));
	*L = (LinkList)malloc(sizeof(Node));
	
	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;
}

注意L和r的關系,L是指整個單鏈表,而r是指向尾節點的變量,r會隨着循環不斷的變化節點,而L則是隨着循環增長為一個多節點的鏈表。

測試代碼:

int main()
{
	LinkList list;
	CreateListTail(&list, 5); /*創建一個有5個節點的單鏈表(不包含頭結點)*/
}

運行結果:

mark

動畫模擬:

mark

3.1.6單鏈表的整表刪除

單鏈表整表刪除的思路:

  1. 聲明一節點p和q

  2. 將一個節點賦值給p

  3. 循環

    • 將下一節點賦值給q

    • 釋放p

    • 將q賦值給p

代碼實現:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int ElemType;
typedef struct Node
{
	ElemType data;
	struct Node *next;
} Node;
typedef struct Node *LinkList;

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

測試代碼:

int main()
{
	LinkList list;
	CreateListTail(&list, 5); /*用尾插法創建一個5個元素的單鏈表*/
	ClearList(&list);	/*清空單鏈表*/
}

運行結果:

mark

動畫模擬:

mark

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

  • 存儲分配方式
    • 順序存儲結構用一段連續的存儲單元依次存儲線性表的數據元素
    • 單鏈表采用鏈式存儲結構,用一組任意的存儲單元存儲線性表的元素
  • 時間性能
    • 查找
      • 順序存儲結構O(1)
      • 單鏈表O(n)
    • 插入和刪除
      • 順序存儲結構O(n)
      • 單鏈表O(1)
  • 空間性能
    • 順序存儲結構需要預先分配存儲空間,分大了浪費空間,分小了容易造成內存溢出
    • 單鏈表不需要分配存儲空間,只要有就可以分配,元素個數也不受限制

總結:若線性表需要頻繁查找,很少進行插入和刪除操作時,宜采用順序存儲結構;若線性表頻繁的進行插入和刪除操作,或者線性表中的元素個數變化較大,或者根本不知道有多大時,宜采用單鏈表結構。

3.2循環鏈表

將單鏈表中終端節點的指針由空指針改為指向頭節點,就使整個單鏈表形成一個環,這種頭尾相接的單鏈表稱為單循環列表,簡稱循環列表(circular linked list)。

循環列表解決了一個很麻煩的問題:如何從一個節點出發,訪問到鏈表的全部節點。

非空的循環列表:

mark

循環列表帶有頭結點的空鏈表:

mark

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

3.3雙向鏈表

雙向鏈表(double linked list)是在單鏈表的每個節點中,再設置一個指向其前驅節點的指針域

3.3.1雙向鏈表的讀取

雙向鏈表的讀取其實和單鏈表的讀取大同小異,只不過雙向鏈表不用每一次都從頭開始找節點,支持反向查找。

mark

3.3.2雙向鏈表的插入

假設存儲元素e的節點為s,要實現將節點s插入到節點p和p->next之間需要下面幾步,如圖所示:

mark

s->prior = p;			/*把p賦值給s的前驅,如圖①*/
s->next = p->next;		/*將p的后繼節點賦值給s的后繼,如圖②*/
p->next->prior = s;		/*將s賦值給p->next的前驅,如圖③*/
p->next = s;		    /*將s賦值給p的后繼,如圖④*/

操作順序是先搞定s的前驅和后繼,再搞定后節點的前驅,最后解決前節點的后繼。順序很重要,不能顛倒

代碼實現:

#define OK 1
#define ERROR 0

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

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操作結果:在L中第i個節點位置之前插入新的數據元素e,L的長度加1*/
Status DulListInsert(DulLinkList *L, int i, ElemType e)
{
	int j;
	DulLinkList p = *L;
	j = 1;

	while (p && j<i)	/*尋找第i-1個節點*/
	{
		p = p->next;
		++j;
	}
	if (!p || j > i)
	{
		return ERROR;	/*第i個節點不存在*/
	}
	DulLinkList s = (DulLinkList)malloc(sizeof(DulNode));	/*生成新節點*/

	s->data = e;
	s->prior = p;				/*把p賦值給s的前驅*/
	s->next = p->next;		/*將p的后繼節點賦值給s的后繼*/
	p->next->prior = s;		/*將s賦值給p->next的前驅*/
	p->next = s;					/*將s賦值給p的后繼*/
	return OK;
}

測試代碼:

int main()
{
	DulLinkList dulList;
	CreateDulListHead(&dulList, 5); /*初始化一個有5個節點的循環鏈表*/
	DulListInsert(&dulList, 3, 7);/*在循環鏈表第3個節點前插入數據7*/
}

運行結果:

mark

我們可以看出循環鏈表一個節點的前驅的后繼或者后繼的前驅都是它自己

p->next->prior = p = p->prior->next

動畫模擬:

mark

3.3.3雙向鏈表的刪除

如果插入操作理解了,那么刪除操作就很簡單了。

假設要刪除節點p,需要下面兩步,如圖所示:

mark

p->prior->next = p->next;	/*將p->next賦值給p->prior的后繼,如圖①*/
p->next->prior = p->prior;	/*將p->prior賦值給p->next的前驅,如圖②*/

代碼實現:

#define OK 1
#define ERROR 0

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

/*初始條件:順序線性表L已經存在,1<=i<=ListLength(L)*/
/*操作結果:刪除L中第i個節點,並用e返回其值,L的長度減1*/
Status DulListDelete(DulLinkList *L, int i, ElemType *e)
{
	int j;
	DulLinkList p = *L;
	j = 1;
	while (p->next && j<i)	/*尋找第i-1個節點*/
	{
		p = p->next;
		++j;
	}
	if (!(p->next) || j > i)
	{
		return ERROR;		/*第i個節點不存在*/
	}
	DulLinkList q = p->next;
	q->prior->next = q->next;		/*將q->next賦值給q->prior的后繼*/
	q->next->prior = q->prior;	/*將q->prior賦值給q->next的前驅*/
	*e = q->data;	/*將q節點中的數據給e*/

	free(q);		/*回收此節點,釋放內存*/

	return OK;
}

測試代碼:

int main()
{
	DulLinkList dulList;
	ElemType e;
	CreateDulListHead(&dulList, 5); /*初始化一個有5個節點的循環鏈表*/
	DulListDelete(&dulList, 3, &e); /*刪除循環鏈表第3個節點並賦值給e*/
}

運行結果:

mark

動畫模擬:

mark

3.3.4雙向循環鏈表

既然單鏈表可以有循環鏈表,那么雙向鏈表當然也可以是循環鏈表。

雙向鏈表的循環帶頭節點的空鏈表

mark

雙向鏈表的循環帶頭節點的非空鏈表

mark


本文為博主學習感悟總結,水平有限,如果不當,歡迎指正。

如果您認為還不錯,不妨點擊一下下方的推薦按鈕,謝謝支持。

轉載與引用請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM