目录
线性表的链式存储结构
基本概念
1、基本概念
链式存储的定义
为了表示每个数据元素与其直接后继元素之间的逻辑关系;数
据元素除了存储本身的信息外,还需要存储其直接后继的信息
链式存储逻辑结构
-基于链式存储结构的线性表中,每个结点都包含数据域和指针域
数据域:存储数据元素本身
指针域:存储相邻结点的地址
专业术语的统—
-顺序表:基于顺序存储结构的线性表
-链表:基于链式存储结构的线性
单链表:每个结点只包含直接后继的地址信息
循环链表:单链表中的最后—个结点的直接后继为第—个结点
双向链表:单链表中的结点包含直接前驱和后继的地址信息
双向循环链表,十字链表 ......
链表中的基本概念
-头结点:链表中的辅助结点,包含指向第一个数据元素的指针
-数据结点:链表中代表数据元素的结点,表现形式为 : (数据元素,地址)
-尾结点:链表中的最后—个数据结点,包含的地址信息为空
单链表中的结点定义
// 单链表的结点的类型
struct Node : public Object
{
T value; // 数据元素的具体类型
Node* next; // 指向后继结点的指针
};
单链表中的内部结构
头结点在单链表中的意义是:辅助数据元素的定位,方便
插入和删除操作;因此,头结点不存储实际的数据元素。
单链表的具体实现与优化
1、单链表的具体实现
LinkList设计要点
-类模板,通过头结点访问后继结点
-定义内部结点类型Node, 用于描述数据域和指针域
-实现线性表的关键操作(增,删,查,等)
一个关键问题:头结点是否也应该用Node定义?
头结点的隐患 :直接用Node创建头节点,头结点的数据域不会使用,若泛指类型为类类型,且类的构造函数抛出异常???所以单独定义头结点,且内存布局与Node一致
// 结点
struct Node : public Object
{
T value;
Node* next;
};
// 头结点
struct : public Object
{
char reserved[sizeof(T)]; // 木有实际作用,仅仅占空间,保证内存布局和Node一样
Node* next;
}m_header;
// 在堆空间创建Node结点,next也会置NULL,符合单链表的特性
2、编程实验
链表的实现与优化 LinkList.h
#ifndef LINKLIST_H
#define LINKLIST_H
#include "List.h"
#include "Exception.h"
namespace DTLib
{
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(int i, const T& e)
{
bool ret = (0 <= i) && (i <= m_length);
if( ret )
{
Node* node = new Node();
if( node )
{
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 insert(const T& e)
{
return insert(m_length, e);
}
bool remove(int i)
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* toDel = position(i)->next;
position(i)->next = toDel->next;
m_length--;
delete toDel;
}
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;
}
bool get(int i, T& e) const
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
e = position(i)->next->value;
}
return ret;
}
T get(int i) const
{
T ret;
if( get(i, ret) )
{
return ret;
}
else
{
THROW_EXCEPTION(IndexOutOfBoundsExpception, "Invalid Parameter i to get element ...");
}
}
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;
}
int length() const
{
return m_length;
}
void clear()
{
while( m_header.next )
{
Node* toDel = m_header.next;
m_header.next = toDel->next;
m_length--;
delete toDel;
}
}
~LinkList()
{
clear();
}
};
}
#endif // LINKLIST_H
main1.cpp
#include <iostream>
#include "LinkList.h"
using namespace std;
using namespace DTLib;
class Test : public Object
{
public:
Test()
{
throw 0;
}
};
int main()
{
LinkList<Test> list0;
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) << " ";
}
list.remove(2);
cout << endl;
for(int i = 0; i < list.length(); i++)
{
cout << list.get(i) << " ";
}
list.clear();
for(int i = 0; i < list.length(); i++)
{
cout << list.get(i) << " ";
}
return 0;
}
main2.cpp
#include <iostream>
#include "LinkList.h"
using namespace std;
using namespace DTLib;
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; // 1
return 0;
}
3、小结
链表中的数据元素在物理内存中无相邻关系
链表中的结点都包含数据域和指针域
头结点用于辅助数据元素的定位,方便插入和删除操作
插入和删除操作需要保证链表的完整性
通过类模板实现链表,包含头结点成员和长度成员
定义结点类型,并通过堆中的结点对象构成链式存储
为了避免构造错误的隐患,头结点类型需要重定义
线性表中元素的查找依赖于相等比较操作符(= =)
代码优化是编码完成后必不可少的环节
单链表的遍历与优化
1、问题
如何遍历单链表中的每—个数据元素?
当前单链表的遍历方法
int main()
{
LinkList<int> list;
for(int i=0; i<5; i++) // O(n)
{
list.insert(0, i); // O(1)
}
for(int i=0; i<list.length(); i++) // O(n²)
{
cout << list.get(i) << endl;
}
return 0;
}
遗憾的事实:不能以线性的时间复杂度完成单链表的遍历
新的需求:为单链表提供新的方法,在线性时间内完成遍历
设计思路(游标)
-在单链表的内部定义—个游标(Node* m_current)
-遍历开始前将游标指向位置为0的数据元素
-获取游标指向的数据元素
-通过结点中的next指针移动游标
提供一组遍历相关的函数,以线性的时间复杂度遍历链表 | |
遍历函数原型设计 | 功能说明 |
bool move(int i, int step = 1) | 将游标定位到目标位置 |
bool end() | 游标是否到达尾部(是否为空) |
T current() | 获取游标所指向的数据元素 |
bool next() | 移动游标 |
单链表内部的—次封装
// 封装create和destroy函数的意义是什么?
virtual Node* create()
{
return new Node();
}
virtual void destroy(Node* pn)
{
delete pn;
}
2、编程实验
单链表的线性遍历 LinkList.h
#ifndef LINKLIST_H
#define LINKLIST_H
#include "List.h"
#include "Exception.h"
namespace DTLib
{
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* m_current;
int m_step;
// 定位到要操作结点的前一个结点位置
Node* position(int i) const
{
Node* ret = reinterpret_cast<Node*>(&m_header);
for(int p = 0; p < i; p++)
{
ret = ret->next;
}
return ret;
}
virtual Node* create()
{
return new Node();
}
virtual void destroy(Node* pn)
{
delete pn;
}
public:
LinkList()
{
m_header.next = 0;
m_length = 0;
m_current = 0;
m_step = 0;
}
bool insert(int i, const T& e)
{
bool ret = (0 <= i) && (i <= m_length);
if( ret )
{
Node* node = create();
if( node )
{
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 insert(const T& e)
{
return insert(m_length, e);
}
bool remove(int i)
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* current = position(i);
Node* toDel = current->next;
if(m_current == toDel)
{
m_current = toDel->next;
}
current->next = toDel->next;
m_length--;
destroy(toDel);
}
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;
}
bool get(int i, T& e) const
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
e = position(i)->next->value;
}
return ret;
}
virtual T get(int i) const
{
T ret;
if( get(i, ret) )
{
return ret;
}
else
{
THROW_EXCEPTION(IndexOutOfBoundsExpception, "Invalid Parameter i to get element ...");
}
}
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;
}
int length() const
{
return m_length;
}
void clear()
{
while( m_header.next )
{
Node* toDel = m_header.next;
m_header.next = toDel->next;
m_length--;
destroy(toDel);
}
}
virtual bool move(int i, int step = 1)
{
bool ret = (0 <= i) && (i < m_length) && (step > 0);
if( ret )
{
m_current = position(i)->next;//游标指向第i个结点
m_step = step;
}
return ret;
}
virtual bool end()
{
return m_current == 0;
}
virtual T current()
{
if( !end() )
{
return m_current->value;
}
else
{
THROW_EXCEPTION(InvalidParameterException, "No value at curent position ...");
}
}
virtual bool next()
{
int i = 0;
while((i < m_step) && !end())
{
m_current = m_current->next;
i++;
}
return i == m_step;
}
~LinkList()
{
clear();
}
};
}
#endif // LINKLIST_H
main.cpp
#include <iostream>
#include "LinkList.h"
using namespace std;
using namespace DTLib;
int main()
{
LinkList<int> list;
for(int i = 0; i < 5; i++)
{
list.insert(0, i);
}
for(list.move(0); !list.end(); list.next())
{
cout << list.current() << " ";
}
cout << endl;
for(list.move(0, 2); !list.end(); list.next())
{
cout << list.current() << " ";
}
cout << endl;
for(list.move(0, 10); !list.end(); list.next())
{
cout << list.current() << endl;
}
return 0;
}
3、小结
单链表的遍历需要在线性时间内完成
在单链表内部定义游标变量,通过游标变量提高效率
遍历相关的成员函数是相互依赖,相互配合的关系
封装结点的申请和删除操作更有利于增强扩展性
静态单链表的实现
1、静态单链表的实现
单链表的—个缺陷
-触发条件:长时间使用单链表对象频繁增加和删除数据元素
- 可能的结果:堆空间产生大量的内存碎片,导致系统运行缓慢
新的线性表
- 在“单链表”的内部增加—片预留的空间,所有的Node对象都在这片空间中动态创建和动态销毁。
静态单链表的继承层次结构
静态单链表的实现思路
-通过模板定义静态单链表类(StaticLinkList)
-在类中定义固定大小的空间(unsigned char[])
-重写create和destroy函数,改变内存的分配和归还方式
-在Node类中重载operator new , 用于在指定内存上创建对象
采用继承定义新结点
typedef typename LinkList<T>::Node Node;
struct SNode : public Node
{
void* operator new (unsigned int size, void* loc) // 在指定的内存上调用构造函数
{
(void)size; // 很多编译器里面,如果你声明了一个变量却没有使用它,编译器会发出警告,使用了就OK
return loc; // 直接返回这片内存地址
}
};
父类的Node类型涉及到泛指类型,引用模板类型参数,所以需要通过LinkList<T>::Node访问
然而,编译时编译器无法确定Node是类型还是静态成员变量,所以加上typename
2、编程实验
静态单链表的实现 StaticLinkList.h
#ifndef STATICLINKLIST_H
#define STATICLINKLIST_H
#include "LinkList.h"
namespace DTLib
{
template <typename T, int N>
class StaticLinkList : public LinkList<T>
{
protected:
typedef typename LinkList<T>::Node Node;
struct SNode : public Node
{
void* operator new (unsigned int size, void* loc)
{
(void)size;
return loc;
}
};
unsigned char m_space[sizeof(SNode) * N];
int m_used[N];
Node* create()
{
SNode* ret = 0;
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] = 1;
break;
}
}
return ret;
}
void destroy(Node* pn)
{
SNode* space = reinterpret_cast<SNode*>(m_space);
SNode* psn = dynamic_cast<SNode*>(pn);
for(int i = 0; i < N; i++)
{
if(psn == (space + i))
{
m_used[i] = 0;
psn->~SNode();
break;
}
}
}
public:
StaticLinkList()
{
for(int i = 0; i < N; i++)
{
m_used[i] = 0;
}
}
int capacity()
{
return N;
}
~StaticLinkList()
{
this->clear(); // this->LinkList<T>::clear();
}
// 析构子类对象时:先调用子类析构函数,再调用父类析构函数
// 调用子类析构函数时,调用自己的destroy
// 调用父类析构函数时,clear里的destroy调用父类的(实际不会调用)
};
}
#endif // STATICLINKLIST_H
main.cpp
#include <iostream>
#include "StaticLinkList.h"
using namespace std;
using namespace DTLib;
int main()
{
StaticLinkList<int,5> list;
for(int i=0; i<list.capacity(); i++)
{
list.insert(0, i);
}
for(list.move(0); !list.end(); list.next())
{
cout << list.current() << endl;
}
return 0;
}
LinkList中封装create和destroy函数的意义是什么?
为静态单链表的实现做准备。
StaticLinkList与LinkList的不同仅在于链表结点内存分配上的不同;
因此,将仅有的不同封装于父类和子类的虚函数中。
3、小结
顺序表与单链表相结合后衍生出静态单链表
静态单链表是LinkList的子类,拥有单链表的所有操作
静态单链表在预留的空间中创建结点对象
静态单链表适合于频繁增删数据元素的场合(最大元素个数固定)
顺序表和单链表的对比分析
1、顺序表和单链表的对比分析
时间复杂度对比分析
有趣的问题
顺序表的整体时间复杂度比单链表要低,那么单链表还有使用价值吗?
效率的深度分析
-实际工程开发中,时间复杂度只是效率的—个参考指标
★ 对于内置基础类型,顺序表和单链表的效率不相上下
★ 对于自定义类类型,顺序表在效率上低于单链表
-插入和删除
★ 顺序表:涉及大量数据对象的复制操作
★ 单链表:只涉及指针操作,效率与数据对象无关
-数据访问
★ 顺序表:随机访问,可直接定位数据对象
★ 单链表:顺序访问,必须从头访问数据对象,无法直接定位
工程开发中的选择
-顺序表
★ 数据元素的类型相对简单,不涉及深拷贝
★ 数据元素相对稳定,访问操作远多于插入和删除操作
-单链表
★ 数据元素的类型相对复杂,复制操作相对耗时
★ 数据元素不稳定,需要经常插入和删除,访问操作较少
2、小结
顺序表适用于访问需求量较大的场合(随机访问)
单链表适用于数据元素频繁插入删除的场合(顺序访问)
当数据类型相对简单时,顺序表和单链表的效率不相上下