數據結構開發(5):線性表的鏈式存儲結構
阿新 • • 發佈:2018-12-14
插入 設計要點 res def cast 解決 數據結構 get move
0.目錄
1.線性表的鏈式存儲結構
2.單鏈表的具體實現
3.順序表和單鏈表的對比分析
4.小結
1.線性表的鏈式存儲結構
順序存儲結構線性表的最大問題是:
- 插入和刪除需要移動大量的元素!如何解決?
鏈式存儲的定義:
- 為了表示每個數據元素與其直接後繼元素之間的邏輯關系;數據元素除了存儲本身的信息外,還需要存儲其直接後繼的信息。
鏈式存儲邏輯結構:
- 基於鏈式存儲結構的線性表中,每個結點都包含數據域和指針域
- 數據域:存儲數據元素本身
- 指針域:存儲相鄰結點的地址
專業術語的統一:
- 順序表
- 基於順序存儲結構的線性表
- 鏈表
- 基於鏈式存儲機構的線性表
- 單鏈表:每個結點只包含直接後繼的地址信息
- 循環鏈表:單鏈表中的最後一個結點的直接後繼為第一個結點
- 雙向鏈表:單鏈表中的結點包含直接前驅和後繼的地址信息
- 基於鏈式存儲機構的線性表
鏈表中的基本概念:
- 頭結點
- 鏈表中的輔助結點,包含指向第一個數據元素的指針
- 數據結點
- 鏈表中代表數據元素的結點,表現形式為:( 數據元素,地址 )
- 尾結點
- 鏈表中的最後一個數據結點,包含的地址信息為空
單鏈表中的結點定義:
單鏈表中的內部結構:
頭結點在單鏈表中的意義是:輔助數據元素的定位,方便插入和刪除操作;因此,頭結點不存儲實際的數據元素。
在目標位置處插入數據元素:
- 從頭結點開始,通過current指針定位到目標位置
- 從堆空間申請新的Node結點
- 執行操作:
node->value = e;
node->next = current->next;
current->next = node;
在目標位置處刪除數據元素:
- 從頭結點開始,通過current指針定位到目標位置
- 使用toDel指針指向需要刪除的結點
- 執行操作:
toDel = current->next;
current->next = toDel->next;
delete toDel;
2.單鏈表的具體實現
本節目標:
- 完成鏈式存儲結構線性表的實現
LinkList 設計要點:
- 類模板,通過頭結點訪問後繼結點
- 定義內部結點類型Node,用於描述數據域和指針域
- 實現線性表的關鍵操作
LinkList的定義:
鏈表的實現 LinkList.h:
#ifndef LINKLIST_H
#define LINKLIST_H
#include "List.h"
#include "Exception.h"
namespace StLib
{
template <typename T>
class LinkList : public List<T>
{
protected:
struct Node : public Object
{
T value;
Node* next;
};
mutable Node m_header;
int m_length;
public:
LinkList()
{
m_header.next = NULL;
m_length = 0;
}
bool insert(const T& e)
{
return insert(m_length, e);
}
bool insert(int i, const T& e)
{
bool ret = ((0 <= i) && (i <= m_length));
if( ret )
{
Node* node = new Node();
if( node != NULL )
{
Node* current = &m_header;
for(int p=0; p<i; p++)
{
current = current->next;
}
node->value = e;
node->next = current->next;
current->next = node;
m_length++;
}
else
{
THROW_EXCEPTION(NoEnoughMemoryException, "No memory to insert new element ...");
}
}
return ret;
}
bool remove(int i)
{
bool ret = ((0 <= i) && (i < m_length));
if( ret )
{
Node* current = &m_header;
for(int p=0; p<i; p++)
{
current = current->next;
}
Node* toDel = current->next;
current->next = toDel->next;
delete toDel;
m_length--;
}
return ret;
}
bool set(int i, const T& e)
{
bool ret = ((0 <= i) && (i < m_length));
if( ret )
{
Node* current = &m_header;
for(int p=0; p<i; p++)
{
current = current->next;
}
current->next->value = e;
}
return ret;
}
T get(int i) const
{
T ret;
if( get(i, ret) )
{
return ret;
}
else
{
THROW_EXCEPTION(IndexOutOfBoundsException, "Invalid parameter i to get element ...");
}
return ret;
}
bool get(int i, T& e) const
{
bool ret = ((0 <= i) && (i < m_length));
if( ret )
{
Node* current = &m_header;
for(int p=0; p<i; p++)
{
current = current->next;
}
e = current->next->value;
}
return ret;
}
int length() const
{
return m_length;
}
void clear()
{
while ( m_header.next )
{
Node* toDel = m_header.next;
m_header.next = toDel->next;
delete toDel;
}
m_length = 0;
}
~LinkList()
{
clear();
}
};
}
#endif // LINKLIST_H
main.cpp測試
#include <iostream>
#include "LinkList.h"
using namespace std;
using namespace StLib;
int main()
{
LinkList<int> list;
for(int i=0; i<5; i++)
{
list.insert(0, i);
list.set(0, i*i);
}
for(int i=0; i<list.length(); i++)
{
cout << list.get(i) << endl;
}
cout << endl;
list.remove(2);
for(int i=0; i<list.length(); i++)
{
cout << list.get(i) << endl;
}
cout << endl;
list.clear();
for(int i=0; i<list.length(); i++)
{
cout << list.get(i) << endl;
}
return 0;
}
運行結果為:
16
9
4
1
0
16
9
1
0
問題:
- 頭結點是否存在隱患?
- 實現代碼是否需要優化?
頭結點的隱患:
代碼優化:
- insert,remove,get,set等操作都涉及元素定位。
代碼優化(LinkList.h):
#ifndef LINKLIST_H
#define LINKLIST_H
#include "List.h"
#include "Exception.h"
namespace StLib
{
template <typename T>
class LinkList : 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;
Node* position(int i) const
{
Node* ret = reinterpret_cast<Node*>(&m_header);
for(int p=0; p<i; p++)
{
ret = ret->next;
}
return ret;
}
public:
LinkList()
{
m_header.next = NULL;
m_length = 0;
}
bool insert(const T& e)
{
return insert(m_length, e);
}
bool insert(int i, const T& e)
{
bool ret = ((0 <= i) && (i <= m_length));
if( ret )
{
Node* node = new Node();
if( node != NULL )
{
Node* current = position(i);
node->value = e;
node->next = current->next;
current->next = node;
m_length++;
}
else
{
THROW_EXCEPTION(NoEnoughMemoryException, "No memory to insert new element ...");
}
}
return ret;
}
bool remove(int i)
{
bool ret = ((0 <= i) && (i < m_length));
if( ret )
{
Node* current = position(i);
Node* toDel = current->next;
current->next = toDel->next;
delete toDel;
m_length--;
}
return ret;
}
bool set(int i, const T& e)
{
bool ret = ((0 <= i) && (i < m_length));
if( ret )
{
position(i)->next->value = e;
}
return ret;
}
T get(int i) const
{
T ret;
if( get(i, ret) )
{
return ret;
}
else
{
THROW_EXCEPTION(IndexOutOfBoundsException, "Invalid parameter i to get element ...");
}
return ret;
}
bool get(int i, T& e) const
{
bool ret = ((0 <= i) && (i < m_length));
if( ret )
{
e = position(i)->next->value;
}
return ret;
}
int length() const
{
return m_length;
}
void clear()
{
while ( m_header.next )
{
Node* toDel = m_header.next;
m_header.next = toDel->next;
delete toDel;
}
m_length = 0;
}
~LinkList()
{
clear();
}
};
}
#endif // LINKLIST_H
3.順序表和單鏈表的對比分析
問題
- 如何判斷某個數據元素是否存在於線性表中?
遺失的操作——find:
- 可以為線性表( List )增加一個查找操作
- int find(const T& e) const;
- 參數:
- 待查找的數據元素
- 返回值:
=0:數據元素在線性表中第一次出現的位置
- -1:數據元素不存在
- 參數:
數據元素查找示例:
實現查找find函數:
在List.h中加入
virtual int find(const T& e) const = 0;
在SeqList.h中加入
int find(const T& e) const
{
int ret = -1;
for(int i=0; i<m_length; i++)
{
if( m_array[i] == e )
{
ret = i;
break;
}
}
return ret;
}
在LinkList.h中加入
int find(const T& e) const
{
int ret = -1;
int i = 0;
Node* node = m_header.next;
while ( node )
{
if( node->value == e )
{
ret = i;
break;
}
else
{
node = node->next;
i++;
}
}
return ret;
}
但是若用類對象來進行測試,會有嚴重的bug:
#include <iostream>
#include "LinkList.h"
using namespace std;
using namespace StLib;
class Test
{
int i;
public:
Test(int v = 0)
{
i = v;
}
};
int main()
{
Test t1;
Test t2;
Test t3;
LinkList<Test> list;
return 0;
}
編譯錯誤信息:
error C2678: 二進制“==”: 沒有找到接受“Test”類型的左操作數的運算符(或沒有可接受的轉換)
於是應該在頂層父類Object中實現重載比較操作符
Object.h
bool operator == (const Object& obj);
bool operator != (const Object& obj);
Object.cpp
bool Object::operator == (const Object& obj)
{
return (this == &obj);
}
bool Object::operator != (const Object& obj)
{
return (this != &obj);
}
main.cpp再測試
#include <iostream>
#include "LinkList.h"
using namespace std;
using namespace StLib;
class Test : public Object
{
int i;
public:
Test(int v = 0)
{
i = v;
}
bool operator == (const Test& t)
{
return (i == t.i);
}
};
int main()
{
Test t1(1);
Test t2(2);
Test t3(3);
LinkList<Test> list;
list.insert(t1);
list.insert(t2);
list.insert(t3);
cout << list.find(t2) << endl;
return 0;
}
運行結果為:
1
時間復雜度對比分析:
有趣的問題:
- 順序表的整體時間復雜度比單鏈表要低,那麽單鏈表還有使用價值嗎?
效率的深度分析:
- 實際工程開發中,時間復雜度只是效率的一個參考指標
- 對於內置基礎類型,順序表和單鏈表的下效率不相上
- 對於自定義類類型,順序表在效率上低於單鏈表
- 插入和刪除
- 順序表:涉及大量數據對象的復制操作
- 單鏈表:只涉及指針操作,效率與數據對象無關
- 數據訪問
- 順序表:隨機訪問,可直接定位數據對象
- 單鏈表:順序訪問,必須從頭訪問數據對象,無法直接定位
工程開發中的選擇:
- 順序表
- 數據元素的類型相對簡單,不涉及深拷貝
- 數據元素相對穩定,訪問操作遠多於插入和刪除操作
- 單鏈表
- 數據元素的類型相對復雜,復制操作相對耗時
- 數據元素不穩定,需要經常插入和刪除,訪問操作較少
4.小結
- 鏈表中的數據元素在物理內存中無相鄰關系
- 鏈表中的結點都包含數據域和指針域
- 頭結點用於輔助數據元素的定位,方便插入和刪除操作
- 插入和刪除操作需要保證鏈表的完整性
- 通過類模板實現鏈表,包含頭結點成員和長度成員
- 定義結點類型,並通過堆中的結點對象構成鏈式存儲
- 為了避免構造錯誤的隱患,頭結點類型需要重定義
- 代碼優化是編碼完成後必不可少的環節
- 線性表中元素的查找依賴於相等比較操作符( == )
- 順序表適用於訪問需求量較大的場合( 隨機訪問 )
- 單鏈表適用於數據元素頻繁插入刪除的場合( 順序訪問 )
- 當數據類型相對簡單時,順序表和單鏈表的效率不相上下
數據結構開發(5):線性表的鏈式存儲結構