2.3 線性表的鏈式儲存結構(連結串列)
基本概念和特點
連結串列的定義
-線性表的鏈式儲存結構稱之為連結串列(linked list)。連結串列包括兩部分構成:資料域和指標域。資料域儲存資料元素,指標域描述資料元素的邏輯關係。
- 連結串列通常使用帶頭結點的表示。指向頭結點的指標稱之為頭指標(head pointer),指向最後一個結點(也就是尾結點)的指標稱之為尾指標(tail pointer)。
- 連結串列不具備隨機存取特性。
- 儲存密度(storage density)是指資料的體積佔結點總體積的比。連結串列的儲存密度總小於1(因為有指標域)。
連結串列的型別及其特點
- 連結串列根據指標域的指向,分為單鏈表、雙鏈表。單鏈表只能通過前驅找到後繼,不能通過後繼找到前驅。雙鏈表使用雙向指標域,方便了反向查詢,是一種時空權衡的做法。
- 根據尾結點和頭結點能否構成聯絡,分為普通連結串列和迴圈連結串列。
單鏈表的表示和基本操作
單鏈表的表示
因為連結串列本身依然是屬於線性結構,也是線性表,因此類介面與線性表無異。線性表具備的方法都應該滿足(但是不意味著能夠以最高效率實現)。
下面是使用C++語言的類描述。開始定義兩個巨集來表示初始化是使用頭插法還是尾插法。
定義結點採用了類的巢狀,因為要保證結點的資料型別和主類一致,使用巢狀類能夠避免很多資料型別不相容的情況。STL就是這麼做的。除了這種方法能夠實現資料結構的泛型,對於C,因為沒有模板,而資料結構通常作為底層的庫,必須具備泛型特性,因此通常直接在記憶體中根據大小存放資料,實際使用時,再將其指標進行強制型別轉換為需要的指標。例如下面給出的php 7的連結串列結點實現(使用變長結構),就是基於這樣的考慮。
/* php v7.1.4 zend_llist.h */
typedef struct _zend_llist_element {
struct _zend_llist_element *next;
struct _zend_llist_element *prev;
char data[1]; /* Needs to always be last in the struct */
} zend_llist_element;
連結串列的終止通常用巨集NULL
來表示,對於C++11,可以使用關鍵字nullptr
來實現空指標。下面的程式碼都是使用了這一特性的,對於不支援C++11的編譯器,可以使用巨集#define nullptr NULL
我們需要維護其頭指標來標識整個連結串列,因此設定了
_head
私有成員變數。_length
是為了簡化求長度的操作,這樣就不必在使用length()
方法時,遍歷整個表了。只需要額外的4位元組開銷。這種方法的壞處在於,不能夠在類外隨便操作連結串列結點(因為無法更新_length
的值)。不過此處的連結串列依然作為線性表的表示(而算一個玩具),所以並無大礙。
//定義建構函式採用頭插法還是尾插法
#define INIT_INSERT_HEAD 1
#define INIT_INSERT_TAIL 0
/**
* 連結串列類
*/
template<typename _Ty>
class SinglyLList
{
public:
/**
* 連結串列結點結構體
*/
struct SinglyLListNode{
_Ty data; //資料域
SinglyLList<_Ty>::SinglyLListNode * next; //指標域
SinglyLListNode(){
next = nullptr;
}
};
private:
SinglyLList<_Ty>::SinglyLListNode * _head; //連結串列的頭指標
int _length; //連結串列的長度(元素個數)
public:
/**
* 建構函式,建立一個線性表,並用指定的資料填充線性表。
* @param _Ty init_data[] 初始輸入資料陣列
* @param int n 初始資料陣列長度
* @param int mode 採用的初始化插入方法,預設頭插法
*/
SinglyLList(_Ty init_data[], int n, int mode = INIT_INSERT_HEAD);
/**
* 預設建構函式,建立一個空的線性表
*/
SinglyLList();
/**
* 解構函式,銷燬線性表
*/
~SinglyLList();
/**
* 判斷當前線性表是否為空
* @return bool 表空返回true,否則false
*/
bool empty();
/**
* 返回當前線性表的長度
* @return int 線性表的實際長度
*/
int length();
/**
* 返回線性表中指定位置的元素
* @param int i 序號
* @return _Ty 返回元素的值
*/
_Ty & at(int i);
_Ty & operator[](int i);
/**
* 查詢線性表中指定值的元素的位置
* @param _Ty value 需要查詢的元素的值
* @return int 返回該元素的位置,0為未找到
*/
int find(_Ty value);
/**
* 將指定元素插入指定位置
* @param int i 待插入元素的位置
* @param _Ty value 待插入元素的值
* @return bool 操作成功返回true,否則false
*/
bool insert(int i, _Ty value);
/**
* @param int i 需要刪除的元素的位置
* @return bool 操作成功返回true,否則false
*/
bool remove(int i);
};
單鏈表的建構函式
建構函式根據已有資料建立整個連結串列。這裡使用了mode
引數決定採用尾插法還是頭插法。頭插法關注的是_head
指標,而尾插法關注的是尾指標,因此維護一個tail
是必要的。單鏈表的插入,先連結當前結點的next
,將其指向下一個結點,然後將原來的前驅結點的next
設定為當前插入的結點。後面的連結串列插入也是如此方法。
template<typename _Ty>
SinglyLList<_Ty>::SinglyLList(_Ty init_data[], int n, int mode){
assert(init_data && n >= 0);
//需要#include<cassert>
SinglyLListNode * tail;
SinglyLListNode * newnode;
tail = _head = new SinglyLListNode;
//判斷輸入資料是否為空
if(n){
for(int i = 0; i < n; i++){
newnode = new SinglyLListNode;
newnode->data = init_data[i];
//判斷使用頭插法還是尾插法
if(mode == INIT_INSERT_HEAD){
newnode->next = _head->next;
_head->next = newnode;
}else{
tail->next = newnode;
tail = newnode;
}
}
}
//如果SinglyLListNode不具備建構函式,下面的程式碼是必須的
//if(mode == INIT_INSERT_TAIL)
// tail->next = nullptr;
_length = n;
}
無引數的建構函式用於建立一個空表,只需要將_length
設定為零,建立一個頭結點就可以了
template<typename _Ty>
SinglyLList<_Ty>::SinglyLList(){
_head = new SinglyLListNode;
_length = 0;
}
解構函式
解構函式用來銷燬整個連結串列,並釋放記憶體空間。主要的方法就是遍歷整個表,逐個銷燬結點。需要注意的是,不能將指標弄丟了,否則會造成記憶體洩漏。在具體操作過程中,就是需要將當前結點的next
先儲存,而後才能釋放當前結點。另外,不能忘記釋放頭結點。
template<typename _Ty>
SinglyLList<_Ty>::~SinglyLList(){
SinglyLListNode * tnode, * nnode = _head;
while(nnode != nullptr){
tnode = nnode->next;
delete nnode;
nnode = tnode;
}
}
判斷連結串列是否是空表
直接判斷_length
域就可以了。判斷_head->next
是否為nullptr
也是可以的。
template<typename _Ty>
bool SinglyLList<_Ty>::empty(){
return _length == 0;
}
得到連結串列的長度
考慮設定_length
域的意義,直接返回_length
域就可以了,否則需要遍歷整個連結串列。
template<typename _Ty>
int SinglyLList<_Ty>::length(){
return _length;
}
根據邏輯序號得到元素
鏈式儲存結構不具備直接得到序號的能力,因此需要在遍歷過程中自行維護一個計數器count
,表示當前結點的序號。注意2.1節線性表的定義中,邏輯序從1開始,因此可以假定沒有儲存實際資料的頭指標的邏輯序為0,以簡化操作。實際上從第一個結點以1計算下標,也是正確的。
template<typename _Ty>
_Ty & SinglyLList<_Ty>::at(int i){
assert(i > 0 && i <= _length);
int count = 0;
SinglyLListNode * pnode = _head;
while(count != i){
pnode = pnode->next;
count++;
}
return pnode->data;
}
為了使邏輯序更清晰,像順序表一樣也過載operator[]
。
template<typename _Ty>
_Ty & SinglyLList<_Ty>::operator[](int i){
return this->at(i);
}
查詢指定元素的邏輯序
查詢指定元素,一樣需要維護一個計數器。一旦找到,則返回其邏輯序,反之,返回0。對於連結串列,實際上返回邏輯序,並無太大用途,單鏈表更傾向返回一個當前結點的前驅,以便於進行插入和刪除操作。後面的改進會提到這一點,以及如何利用這一點提高某些演算法的效率。
template<typename _Ty>
int SinglyLList<_Ty>::find(_Ty value){
int count = 1;
SinglyLListNode * pnode = _head->next;
while(pnode != nullptr){
if(pnode->data == value)
return count;
pnode = pnode->next;
count++;
}
return 0;
}
插入元素(結點)
插入元素,首先根據邏輯序找到適當的位置。單鏈表不能夠通過某一結點得到前驅,因此需要插入到第_length
需要自增。
template<typename _Ty>
bool SinglyLList<_Ty>::insert(int i, _Ty value){
assert(i > 0 && i <= _length + 1);
int count = 0;
//先查詢到適當的位置(需要插入的位置的前一個結點)
SinglyLListNode * pnode = _head;
while(count != i - 1){
pnode = pnode->next;
count++;
}
SinglyLListNode * newnode = new SinglyLListNode;
newnode->data = value;
newnode->next = pnode->next;
pnode->next = newnode;
_length++;
return true;
}
刪除結點
刪除結點同插入較為類似,也要查詢實際結點的前驅。別忘了_length
自減。
template<typename _Ty>
bool SinglyLList<_Ty>::remove(int i){
assert(i > 0 && i <= _length);
int count = 0;
//先查詢到適當的位置(需要插入的位置的前一個結點)
SinglyLListNode * pnode = _head;
while(count != i - 1){
pnode = pnode->next;
count++;
}
SinglyLListNode * tmp = pnode->next;
pnode->next = pnode->next->next;
delete tmp;
_length--;
return true;
}
測試程式碼
int main(){
int a[] = {10, 11, 12, 13, 14};
SinglyLList<int> s(a, 5);
int i;
for(i = 1; i <= 4; i++)
s.insert(1, i);
printf(s.empty() ? "SinglyLList Is empty.\n" : "SinglyLList Not Empty.\n");
for(i = 1; i <= s.length(); i++)
printf("%d ", s.at(i));
printf("\n");
s.remove(1);
for(i = 1; i <= s.length(); i++)
printf("%d ", s.at(i));
printf("\n");
s.remove(3);
for(i = 1; i <= s.length(); i++)
printf("%d ", s.at(i));
s.insert(3, 5);
s.insert(3, 6);
printf("\nLength => %d\n", s.length());
for(i = 1; i <= s.length(); i++)
printf("%d ", s.at(i));
printf("SinglyLList[2] => %d\n", s[2]);
printf("5 is Found At => %d\n", s.find(5));
getchar();
return 0;
}
輸出
SinglyLList Not Empty.
4 3 2 1 14 13 12 11 10
3 2 1 14 13 12 11 10
3 2 14 13 12 11 10
Length => 9
3 2 6 5 14 13 12 11 10 SinglyLList[2] => 2
5 is Found At => 4
一些改進
很多基於連結串列的演算法是先查詢再進行插入/刪除操作,實際上,對於連結串列的底層直接操作是十分必要的。
因此可以做一些改進,直接提供_head
結點,並將length()
方法的返回值變成引用型別,這樣就可以很容易在外部直接操作連結串列了。
更好的做法是採用在2.1節提到過的迭代器。對於非順序儲存結構而言,因為不能進行隨機存取操作,故使用邏輯序並不能帶來操作上的高效性,直接使用物理指標又破壞了資料結構的封裝性。因此像STL的做法,採用迭代器進行包裝,通過迭代器直接維護相關地址資訊,在主類中直接就可以對迭代器而非邏輯序號進行操作,既隱藏了連結串列的物理實現,避免直接操作連結串列導致錯誤,也能夠充分利用連結串列特點,避免反覆從頭查詢的壞處。
使用迭代器的具體做法,可以參考侯捷的《STL原始碼剖析》。
單鏈表應用的一些例子
基於單鏈表設計演算法,有兩種思路:
- 先查詢再進行相關操作(插入/刪除);
- 使用建表操作,對資料進行處理。
下面分別舉兩個例子。將函式作為類公有成員,這樣可以直接操作連結串列類的私有成員變數。
連結串列逆序
已知一個鏈式儲存的線性表
例如1->2->3->4->5->NULL
應該變成5->4->3->2->1
。時間複雜度要求為
提到逆序問題不難想到直接利用連結串列的頭插法解決。於是程式碼就很簡單了。之前考慮使用C++11的auto
特性,但是不便於體現資料結構的特點,並且此處並不算繁雜,因此不用,直接寫SinglyLListNode *
。
//類宣告略
/**
* 連結串列逆序演算法
*/
template<typename _Ty>
void SinglyLList<_Ty>::reverse(){
SinglyLListNode * p = _head->next;
while(p != nullptr){
SinglyLListNode * q = p->next;
p->next = _head->next;
_head->next = p;
p = q;
}
return;
}
連結串列排序
給定一個鏈式儲存結構
例如5->4->3->1->2->NULL
應該得到1->2->3->4->5->NULL
。
template<typename _Ty>
void SinglyLList<_Ty>::sort(){
SinglyLListNode * p = _head->next;
_head->next = nullptr;
while(p != nullptr){
SinglyLListNode * q1 = _head->next; //作為比較指標
SinglyLListNode * q2 = _head; //q1的前驅指標
SinglyLListNode * r = p->next; //儲存下一個結點的指標
while(q1 != nullptr){
if(q1->data > p->data)
break;
q2 = q1;
q1 = q1->next;
}
q2->next = p;
p->next = q1;
p = r;
}
return;
}
下面是一種空間複雜度為comp_func
的演算法)的一種演算法。其核心思想在於用一個臨時陣列elements
儲存所有結點的指標,然後就可以使用陣列排序演算法comp_func
對elements
進行排序,排序完成後,恢復指標域的指向(根據elements[]
的已經排序的特點)即可。雖然連結串列也可以直接實現
/* php v7.1.4 zend_llist.c */
ZEND_API void zend_llist_sort(zend_llist *l, llist_compare_func_t comp_func)
{
size_t i;
zend_llist_element **elements;
zend_llist_element *element, **ptr;
if (l->count <= 0) {
return;
}
elements = (zend_llist_element **) emalloc(l->count * sizeof(zend_llist_element *));
ptr = &elements[0];
for (element=l->head; element; element=element->next) {
*ptr++ = element;
}
zend_sort(elements, l->count, sizeof(zend_llist_element *),
(compare_func_t) comp_func, (swap_func_t) zend_llist_swap);
l->head = elements[0];
elements[0]->prev = NULL;
for (i = 1; i < l->count; i++) {
elements[i]->prev = elements[i-1];
elements[i-1]->next = elements[i];
}
elements[i-1]->next = NULL;
l->tail = elements[i-1];
efree(elements);
}
雙鏈表
雙鏈表由於多了一個prev
域,因此在插入刪除方面比單鏈表複雜了一些。為了與單鏈表一致,通常情況也是找需要的結點的前驅。事實上,找本身也是可以的,因為雙鏈表可以隨意尋找其前驅和後繼。總之,原則就是,只要不把指標弄丟了,都是OK的。
以插入為例,已知雙鏈表的某一結點為p
,則在p
之後插入一個結點s
,需要修改4個指標域:
s->next = p->next; //①
p->next->prev = s; //②
s->prev = p; //③
p->next = s; //④
以不弄丟結點為原則,①②是可以交換的,③④也是可以交換的。然而①②作為整體不能與③④交換,因為如果交換,那麼p
的後繼就被弄沒了(畫個圖應該很清晰)。
當然,如果在p
的前面插入一個結點s
,就應該先將s
連結到p->prev
上了。畫個圖同樣很清晰為什麼。
迴圈連結串列
迴圈連結串列的特點在於,能夠根據尾指標找到頭指標。因此,對於需要同時頻繁操作首尾元素的情形,比如實現一個雙端佇列deque
,雙向迴圈連結串列能夠提供較大的效率。STL的連結串列容器,為了提供諸如push_front
,push_back
的操作,使用了雙向迴圈連結串列實現。
除此之外,迴圈連結串列還可以用來很方便的模擬約瑟夫環問題。(然而實際高效求解約瑟夫環問題,是求遞推式)
Summary
- 理解連結串列的優缺點
連結串列就是線性表的鏈式儲存結構,優點是
O(1) 實現插入和刪除操作,缺點是不能隨機存取(查詢某個序號的元素O(n) )。
連結串列的儲存密度相比順序儲存結構較小,但是有更大的靈活性(能夠充分利用碎片空間) 實現連結串列這一資料結構及插入刪除操作
實現連結串列的基本操作不復雜,核心點在於如何實現遍歷。對於初學者而言與普通迴圈的終止不同,連結串列的終點在於其
next
指向空指標。
連結串列的插入和刪除需要修改指標的指向,單鏈表、雙鏈表各有其不同的方法,根據已知結點的情況的不同實際操作千差萬別。其實只需要保證連結串列不斷裂,沒有結點脫離了表就可以了,畫個圖就行了,不必死記硬背先連線哪個後連線哪個。實現一些基於連結串列的基本演算法
基於連結串列的演算法,通常是基於建表的,核心就是圍繞頭插法還是尾插法。
除此之外,連結串列的另一種核心演算法就是基於查詢的。注意單鏈表必須去查詢其前驅結點,而雙鏈表通常基於習慣,也是查詢其前驅結點。
線性表的大多數演算法,連結串列都能夠實現。
相關推薦
2.3 線性表的鏈式儲存結構(連結串列)
基本概念和特點 連結串列的定義 -線性表的鏈式儲存結構稱之為連結串列(linked list)。連結串列包括兩部分構成:資料域和指標域。資料域儲存資料元素,指標域描述資料元素的邏輯關係。 - 連結串列通常使用帶頭結點的表示。指向頭結點的指標稱之為頭指標
線性表——鏈式儲存結構合併操作
採取的結構和上一篇博文一致,均為單鏈表儲存結構。#include<iostream> #include<stdio.h> #include<stdlib.h> #define ElemType int #define Status
3.3 棧的鏈式儲存結構
<?php header("content-type:text/html;charset=utf-8"); /** * 棧的鏈式儲存結構的基本操作 * *包括 * 1.初始化 __contruct() * 2.進棧操作 push() * 3.出棧操作 pop() * 4.銷燬棧 de
大話資料結構 —— 3.6 線性表的鏈式儲存結構
3.6.1 順序儲存結構不足的解決辦法 C同學:反正要在相鄰元素間留多少空間都是有可能不夠的,那不如乾脆不要考慮相鄰位置這個問題了。哪裡有空位就放在哪裡,此時指標剛好可以派上用場。 每個元素多用一個
3.線性表的鏈式儲存結構————靜態連結串列(C語言和C++完整解析)
目錄 1.靜態連結串列的概念 因為有些語言沒有指標,所以難以實現普通連結串列,靜態連結串列就是用來解決這一問題的有力工具,靜態連結串列使用陣列來實現連結串列。靜態連結串列用遊標來代替普通連結串列的指標域,並且用下標代替普通連結串列的結點
2.線性表的鏈式儲存結構————單鏈表(思路分析,C語言、C++完整程式)
目錄 1.單鏈表的基本概念 (1)單鏈表:當連結串列中的每個結點只含有一個指標域時,稱為單鏈表。 (2)頭指標:如上圖所示,連結串列中第一個結點的儲存位置叫做頭指標。 (3)頭結點:頭結點是放在第一個元素結點之前的結點,頭結點不是連結串列中的必
走進資料結構和演算法(c++版)(3)——線性表的鏈式儲存結構
線性表的鏈式儲存結構 我們知道線性表的順序儲存結構在插入和刪除操作時需要移動大量的資料,他們的時間複雜度為O(n)O(n)。當我們需要經常插入和刪除資料時,順序儲存結構就不適用了,這時我們就需要用到線性表的鏈式儲存結構。 線性表的鏈式儲存結構的特點是
資料結構 筆記:線性表的鏈式儲存結構
鏈式儲存的定義 為了表示每個資料元素與其直接後繼元素之間的邏輯關係;資料元素出了儲存本身的資訊外,還需要儲存直接後繼的資訊。 ps:在邏輯上,元素之間是相鄰的;在實體記憶體中元素之間並無相鄰關係。 鏈式儲存邏輯結構 -基礎鏈式儲存結構的線性表中,每個節點都包含資料域和指標域 ·資
資料結構線性表之鏈式儲存結構單鏈表(C++)
一. 標頭檔案—linkedlist.h 1 #ifndef _LIKEDLIST_H_ 2 #define _LIKEDLIST_H_ 3 4 #include <iostream> 5 6 template <class T> 7 struc
線性表的鏈式儲存結構的基本操作(經編譯)
/* 連結串列銷燬的時候,是先銷燬了連結串列的頭,然後接著一個一個的把後面的結點銷燬了,這樣這個連結串列就不能再使用了。 連結串列清空的時候,是先保留了連結串列的頭,然後把頭後面的所有的結點都銷燬,最後把頭裡指向下一個的指標設為空,這樣就相當與清空了,但這個連結
【資料結構】線性表的鏈式儲存結構--單鏈表
1. 線性表的鏈式儲存結構 鏈式儲存:用一組任意的儲存單元儲存線性表中的資料元素。用這種方法儲存的線性表簡稱線性連結串列。 儲存連結串列中結點的一組任意的儲存單元可以是連續的,也可以是不連續的,甚至是零散分佈在記憶體中的任意位置上的。 連結串列中結點的邏輯順序和物理順序不
線性表包括順序儲存結構和鏈式儲存結構
還記得資料結構這個經典的分類圖吧: 今天主要關注一下線性表。 什麼是線性表 線性表的劃分是從資料的邏輯結構上進行的。線性指的是在資料的邏輯結構上是線性的。即在資料元素的非空有限集中 (1) 存在唯一的一個被稱作“第一個”的資料元素,(2) 存在唯一的一個被稱
(一)線性表的鏈式儲存結構
2.3.1 線性表的鏈式儲存結構——連結串列 連結串列: 1.每個節點中除資料域外,設定了一個指標域,用以指向其後繼節點,這樣構成的連結表稱為線性單向連結串列,簡稱單鏈表。 2.每個節點中除資料域外,設定兩個指標域,分別用以指向其前驅節點和
線性表的鏈式儲存結構從建表插入到 刪除銷燬
檔案1 list.c#include<stdio.h> #include"list.h" #include<stdlib.h> #include<string.h> /* 函式名 creatList; 功能 建立連結串列 申請空間
線性表的鏈式儲存結構
鏈式儲存定義: 為了表示每個資料元素與其直接後繼元素之間的邏輯關係,每個元素除了儲存本身的資訊外,還需要儲存指示其直接後繼的資訊。 單鏈表包括: 表頭結點:連結串列中的第一個結點,包含指向第一個資料元素的指標以及連結串列自身的一些資訊。 資料結點:連結串列中代表資料元素的
C語言實現線性表的鏈式儲存結構
線性表的鏈式儲存結構 特點 結點除自身的資訊域外,還有表示關聯資訊的指標域。因此,鏈式儲存結構的儲存密度小、儲存空間利用率低。 在邏輯上相鄰的結點在物理上不必相鄰,因此,不可以隨機存取,只能順序存取。 插入和刪除操作方便靈活,不必移動結點只需修改結點
線性表、棧、佇列的鏈式儲存結構
一、順序儲存結構與鏈式儲存結構的區別 順序儲存就是從記憶體中取出一段連續地址的空間,將資料依次連續的儲存在這段空間中。而鏈式儲存結構是指資料儲存在記憶體中的地址是離散的,以資料節點為單
線性表在鏈式儲存結構下的基本操作
原始碼: #include<iostream> #include<cstring> #define OK 1 #define ERROR 0 #define OVE
程式設計實現順序儲存結構和鏈式儲存結構線性表的建立、查詢、插入、刪除等基本操作
#include <stdio.h> #include <stdlib.h> typedef struct LNode{ int data; //連結串列資料 struct LNode* next; //連結串列指標 }LNode,*L
資料結構一一線性表的鏈式儲存結構之插入與遍歷
#include <iostream> #include <stdio.h> #include <time.h> #include <malloc.h> #define ERROR 0 #define OK 1 typedef int Status;/*