數據結構(五)——單鏈表
一、單鏈表簡介
1、單鏈表簡介
單鏈表設計要點:
A、類模板,通過頭結點訪問後繼結點。
B、定義內部結點類型,用於描述鏈表中的結點的數據域和指針域。
C、實現線性表的關鍵操作
2、單鏈表中結點的定義
struct Node:public Object
{
T value;//數據域
Node* next;//指針域
};
3、單鏈表的內部結構
頭結點不存儲實際的數據元素,用於輔助數據元素的定位,方便插入和刪除操作。
4、單鏈表中頭結點的定義
mutable Node m_header;
5、單鏈表類基本結構
template <typename T> class LinkedList:public List<T> { protected: struct Node:public Object { T value;//數據域 Node* next;//指針域 }; Node m_header; int m_length; public: LinkedList(); virtual ~LinkedList(); bool insert(int index, const T& value); bool remove(int index); bool set(int index, const T& value); bool get(int index, T& value)const; int length()const; int find(const T& value)const; void clear(); };
二、單鏈表的操作
1、單鏈表的插入操作
在目標位置處插入數據元素流程如下:
A、從頭結點開始,提供current指針定位到目標位置
B、從堆空間申請新的Node結點
C、將新結點插入目標位置,並連接相鄰結點的邏輯關系。
bool insert(int index, const T& value) { //判斷目標位置是否合法 bool ret = (0 <= index) && (index <= m_length); if(ret) { //創建新的結點 Node* node = new Node(); if(node != NULL) { //初始化當前結點為頭結點 Node* current = &m_header; //遍歷到目標位置 for(int i = 0; i < index; i++) { current = current->next; } //修改插入結點的值 node->value = value; //將當前位置的下一個結點作為插入結點的下一個結點 node->next = current->next; //將要插入結點作為當前結點的下一個結點 current->next = node; m_length++;//鏈表結點長度加1 } else { THROW_EXCEPTION(NoEnoughMemoryException, "No enough memmory..."); } } else { THROW_EXCEPTION(IndexOutOfBoudsException, "Parameter index is invalid..."); } return ret; }
2、單鏈表的刪除操作
在目標位置刪除數據元素的流程:
A、從頭結點開始,通過current指針定位到目標位置
B、使用toDel指針指向要被刪除的結點
C、刪除結點,並連接相鄰結點的邏輯關系
bool remove(int index) { //判斷目標位置是否合法 bool ret = (0 <= index) && (index < m_length); if(ret) { //當前結點指向頭結點 Node* current = &m_header; //遍歷到目標位置 for(int i = 0; i < index; i++) { current = current->next; } //使用toDel指向要刪除的結點 Node* toDel = current->next; //將當前結點的下一個結點指向要刪除結點的下一個結點 current->next = toDel->next; m_length--;//鏈表結點長度減1 delete toDel;//釋放要刪除的結點的堆空間 } else { THROW_EXCEPTION(IndexOutOfBoudsException, "Parameter inde is invalid..."); } return ret; }
3、設置目標結點的值
設置目標結點的值的流程如下:
A、從頭結點開始,通過current指針定位到目標位置
B、修改目標結點的數據域的值
bool set(int index, const T& value)
{
//判斷目標位置是否合法
bool ret = (0 <= index) && (index < m_length);
if(ret)
{
//將當前結點指向頭結點
Node* current = &m_header;
//遍歷到目標位置
for(int i = 0; i < index; i++)
{
current = current->next;
}
//修改目標結點的數據域的值
current->next->value = value;
}
else
{
THROW_EXCEPTION(IndexOutOfBoudsException, "Parameter inde is invalid...");
}
return ret;
}
4、獲取目標結點的值
bool get(int index, T& value)const
{
//判斷目標位置是否合法
bool ret = (0 <= index) && (index < m_length);
if(ret)
{
//當前結點指向頭結點
Node* current = &m_header;
//遍歷到目標位置
for(int i = 0; i < index; i++)
{
current = current->next;
}
//獲取目標結點的數據域的值
value = current->next->value;
}
else
{
THROW_EXCEPTION(IndexOutOfBoudsException, "Parameter inde is invalid...");
}
return ret;
}
//重載版本
T get(int index)const
{
T ret;
get(index, ret);
return ret;
}
5、獲取單鏈表的長度
int length()const
{
return m_length;
}
6、單鏈表的清空操作
void clear()
{
//遍歷刪除結點
while(m_header.next)
{
//要刪除的結點為頭結點的下一個結點
Node* toDel = m_header.next;
//將頭結點的下一個結點指向刪除結點的下一個結點
m_header.next = toDel->next;
delete toDel;//釋放要刪除結點
}
m_length = 0;
}
三、單鏈表的優化
1、單鏈表中頭結點的缺陷
struct Node:public Object
{
T value;//數據域
Node* next;//指針域
};
mutable Node m_header;
由於單鏈表在構建時必須先創建頭結點,頭結點在創建時必須調用具體類型的構造函數,如果具體類型的構造函數拋出異常,則單鏈表對象將構建失敗,並會傳遞具體類型構造函數的異常。
class Test
{
public:
Test()
{
throw 0;
}
};
int main()
{
LinkedList<Test> l;
return 0;
}
因此,為了確保模板類的健壯性,需要對頭結點的創建進行優化,即在創建單鏈表對象時不調用具體類型的構造函數。
mutable struct:public Object
{
char reserved[sizeof(T)];//占位空間
Node* next;
}m_header;
創建的匿名的頭結點m_header的內存布局與Node對象的內存布局完全一樣,並且不會調用具體類型T的構造函數。
2、目標位置定位的代碼優化
單鏈表的操作中經常會定位到目標位置,因此可以將此部分代碼獨立構建一個函數。
Node* position(int index)const
{
//指針指向頭結點
Node* ret = reinterpret_cast<Node*>(&m_header);
//遍歷到目標位置
for(int i = 0; i < index; i++)
{
ret = ret->next;
}
return ret;
}
3、鏈表的元素查找操作
為List模板類增加一個find操作:
virtual int find(const T& value)const = 0;
順序存儲結構的線性表SeqList模板類的find實現如下:
int find(const T& value)const
{
int ret = -1;
//遍歷線性表
for(int i = 0; i < m_length; i++)
{
//如果找到元素,退出循環
if(m_array[i] = value)
{
ret = i;
break;
}
}
return ret;
}
鏈式存儲結構的線性表的find操作如下:
int find(const T& value)const
{
int ret = -1;
//指向頭結點
Node* node = m_header.next;
int i = 0;
while(node)
{
//找到元素,退出循環
if(node->value == value)
{
ret = i;
break;
}
else
{
node = node->next;
i++;
}
}
return ret;
}
4、時間復雜度分析
在實際工程開發中,時間復雜度只是效率的一個參考指標。對於內置基礎類型,順序存儲結構實現的線性表與鏈式存儲結構實現的線性表的效率基本相同。對於自定義類類型,順序存儲結構實現的線性表比鏈式存儲結構實現的線性表效率要低,原因在於順序存儲結構實現的線性表要設計大量的數據元素的復制,如果自定義類型的拷貝耗時嚴重,則效率會很低。
順序存儲結構實現的線性表的插入和刪除操作涉及大量對象的復制操作,鏈式存儲結構實現的線性表的插入和刪除操作只涉及指針的操作,不會涉及數據對象的復制。
順序存儲結構實現的線性表的數據訪問支持隨機訪問,可直接定位數據對象。鏈式存儲結構實現的線性表的數據訪問必須順序訪問,必須從頭開始訪問數據對象,無法直接定位。
因此,順序存儲結構實現的線性表適合數據類型的類型相對簡單,不涉及深拷貝,數據元素相對固定,訪問操作遠多於插入和刪除的場合。鏈式存儲結構實現的線性表適合數據元素的類型復雜,復制操作耗時,數據元素不穩定,需要經常插入和刪除操作,訪問操作較少的場合。
5、單鏈表的遍歷
通常遍歷鏈表的方法時間復雜度為O(n^2)
for(int i = 0; i < ll.length(); i++)
{
ll.get(i);
}
通過使用遊標的方法將遍歷鏈表的時間復雜度優化為O(n):
A、在單鏈表的內部定義一個遊標(Node* m_current)
B、遍歷開始前將遊標指向位置為0的數據元素
C、獲取遊標指向的數據元素
D、通過結點中的next指針移動遊標
提供一組遍歷相關成員函數,以線性時間復雜度遍歷鏈表:
bool move(int pos, int step = 1)
{
bool ret = (0 <= pos) && (pos < m_length) && (0 < step);
if(ret)
{
m_current = position(pos);
m_step = step;
}
return ret;
}
bool end()
{
return m_current == NULL;
}
T current()
{
if(!end())
{
return m_current->value;
}
else
{
THROW_EXCEPTION(InvalidOperationException, "Invalid Operation...");
}
}
bool next()
{
int i = 0;
while((i < m_step) && (!end()))
{
m_current = m_current->next;
i++;
}
return (i == m_step);
}
使用遊標遍歷鏈表的方法:
for(ll.move(0); !ll.end(); ll.next())
{
cout << ll.current() << endl;
}
6、單鏈表的翻轉
單鏈表反轉有遞歸和非遞歸兩種算法。
單鏈表翻轉的遞歸實現:
Node* reverse(Node* list)
{
Node* ret = NULL;
//如果單鏈表為空
if(list == NULL || list->next == NULL)
{
ret = list;
}
else
{
Node* guard = list->next;
ret = reverse(list->next);
guard->next = list;
list->next = NULL;
}
return ret;
}
單鏈表翻轉的非遞歸實現:
Node* reverse(Node *header)
{
Node* guard = NULL;//鏈表翻轉後的頭結點
Node* current = reinterpret_cast<Node*>(header);//當前結點
Node* prev = NULL;//前一結點
while(current != NULL)
{
Node* pNext = current->next;//下一結點
if(NULL == pNext)//如果是單結點鏈表
{
guard = current;
}
current->next = prev;//當前結點的下一個結點指向前一結點,實現翻轉
prev = current;//將前一結點移到當前結點位置
current = pNext;//將當前結點後移
}
return guard;
}
四、單鏈表的實現
template <typename T>
class LinkedList:public List<T>
{
protected:
struct Node:public Object
{
T value;//數據域
Node* next;//指針域
};
mutable struct:public Object
{
char reserved[sizeof(T)];//占位空間
Node* next;
}m_header;
int m_length;
int m_step;
Node* m_current;
protected:
Node* position(int index)const
{
//指針指向頭結點
Node* ret = reinterpret_cast<Node*>(&m_header);
//遍歷到目標位置
for(int i = 0; i < index; i++)
{
ret = ret->next;
}
return ret;
}
public:
LinkedList()
{
m_header.next = NULL;
m_length = 0;
m_step = 1;
m_current = NULL;
}
virtual ~LinkedList()
{
clear();
}
bool insert(int index, const T& value)
{
//判斷目標位置是否合法
bool ret = (0 <= index) && (index <= m_length);
if(ret)
{
//創建新的結點
Node* node = createNode();
if(node != NULL)
{
//當前結點定位到目標位置
Node* current = position(index);
//修改插入結點的值
node->value = value;
//將當前位置的下一個結點作為插入結點的下一個結點
node->next = current->next;
//將要插入結點作為當前結點的下一個結點
current->next = node;
m_length++;//鏈表結點長度加1
}
else
{
THROW_EXCEPTION(NoEnoughMemoryException, "No enough memmory...");
}
}
else
{
THROW_EXCEPTION(IndexOutOfBoudsException, "Parameter index is invalid...");
}
return ret;
}
bool insert(const T& value)
{
return insert(m_length, value);
}
bool remove(int index)
{
//判斷目標位置是否合法
bool ret = (0 <= index) && (index < m_length);
if(ret)
{
//當前結點指向頭結點
Node* current = position(index);
//使用toDel指向要刪除的結點
Node* toDel = current->next;
//將當前結點的下一個結點指向要刪除結點的下一個結點
current->next = toDel->next;
m_length--;//鏈表結點長度減1
destroy(toDel);//釋放要刪除的結點的堆空間
}
else
{
THROW_EXCEPTION(IndexOutOfBoudsException, "Parameter inde is invalid...");
}
return ret;
}
bool set(int index, const T& value)
{
//判斷目標位置是否合法
bool ret = (0 <= index) && (index < m_length);
if(ret)
{
//將當前結點指向頭結點
Node* current = position(index);
//修改目標結點的數據域的值
current->next->value = value;
}
else
{
THROW_EXCEPTION(IndexOutOfBoudsException, "Parameter inde is invalid...");
}
return ret;
}
bool get(int index, T& value)const
{
//判斷目標位置是否合法
bool ret = (0 <= index) && (index < m_length);
if(ret)
{
//當前結點指向頭結點
Node* current = position(index);
//遍歷到目標位置
//獲取目標結點的數據域的值
value = current->next->value;
}
else
{
THROW_EXCEPTION(IndexOutOfBoudsException, "Parameter inde is invalid...");
}
return ret;
}
//重載版本
T get(int index)const
{
T ret;
if(get(index, ret))
return ret;
}
int length()const
{
return m_length;
}
int find(const T& value)const
{
int ret = -1;
//指向頭結點
Node* node = m_header.next;
int i = 0;
while(node)
{
//找到元素,退出循環
if(node->value == value)
{
ret = i;
break;
}
else
{
node = node->next;
i++;
}
}
return ret;
}
void clear()
{
//遍歷刪除結點
while(m_header.next)
{
//要刪除的結點為頭結點的下一個結點
Node* toDel = m_header.next;
//將頭結點的下一個結點指向刪除結點的下一個結點
m_header.next = toDel->next;
m_length--;
destroy(toDel);//釋放要刪除結點
}
}
bool move(int pos, int step = 1)
{
bool ret = (0 <= pos) && (pos < m_length) && (0 < step);
if(ret)
{
m_current = position(pos)->next;
m_step = step;
}
return ret;
}
bool end()
{
return m_current == NULL;
}
T current()
{
if(!end())
{
return m_current->value;
}
else
{
THROW_EXCEPTION(InvalidOperationException, "Invalid Operation...");
}
}
bool next()
{
int i = 0;
while((i < m_step) && (!end()))
{
m_current = m_current->next;
i++;
}
return (i == m_step);
}
virtual Node* createNode()
{
return new Node();
}
virtual void destroy(Node* node)
{
delete node;
}
};
五、靜態單鏈表
1、單鏈表的缺陷
長時間使用單鏈表頻繁增加和刪除數據元素時,堆空間會產生大量內存碎片,導致系統運行緩慢。
2、靜態單鏈表的設計
靜態單鏈表的實現要點:
A、通過類模板實現靜態單鏈表
B、在類中定義固定大小的空間
C、重寫create、destroy函數,改變內存的分配和釋放方式
D、在Node類中重載operator new操作符,用於在指定內存上創建對象。
3、靜態單鏈表的實現
template <typename T, int N>
class StaticLinkedList:public LinkedList<T>
{
protected:
typedef typename LinkedList<T>::Node Node;
struct SNode:public Node
{
//重載new操作符
void* operator new(unsigned int size, void* loc)
{
return loc;
}
};
unsigned char m_space[N*sizeof(Node)];//順序存儲空間
bool m_used[N];//空間可用標識
public:
StaticLinkedList()
{
for(int i = 0; i < N; i++)
{
m_used[i] = false;
}
}
Node* createNode()
{
SNode* ret = NULL;
for(int i = 0; i < N; i++)
{
if(!m_used[i])
{
ret = reinterpret_cast<SNode*>(m_space) + i;
ret = new(ret) SNode();
m_used[i] = true;
break;
}
}
return ret;
}
void destroy(Node* node)
{
SNode* space = reinterpret_cast<SNode*>(m_space);
SNode* pn = dynamic_cast<SNode*>(node);
for(int i = 0; i < N; i++)
{
if(pn == space + i)
{
m_used[i] = false;
pn->~SNode();
}
}
}
int capacty()
{
return N;
}
};
靜態單鏈表擁有單鏈表的所有操作,在預留的順序存儲空間中創建結點對象,適合於最大數據元素個數固定需要頻繁增加和刪除數據元素的場合。
數據結構(五)——單鏈表