八、单链表的实现
1、链式存储结构线性表的实现:
LinkList
设计要点:类模板
- 通过头结点访问后继节点
- 定义内部结点类型Node,用于描述数据域和指针域
- 实现线性表的关键操作(增、删、改、查等)
2、LinkList
template <typename T>
class LinkList : public List<T>
{
protected:
struct Node : public Object
{
T value;
Node* next;
};
Node m_header;
int m_length;
public:
LinkList() {}
};
具体实现
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)
{
// 注意i的范围
bool ret = ((i>=0) && (i<=m_length));
cout << "ret = " << ret << endl;
if (ret)
{
Node* node = new Node();
if (node != NULL)
{
// current的目标指向其实都是目标位置的前一个,比如:在第0个位置增加元素,current指向的是header
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
{
cout << "THROW_EXCEPTION" << endl;
THROW_EXCEPTION(NoEnoughMemoryException, "No memory to insert new element");
}
}
return ret;
}
// 删除指定位置元素
bool remove(int i)
{
// 注意i的范围
bool ret = ((i>=0) && (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)
{
// 注意i的范围
bool ret = ((i>=0) && (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;
}
// get函数用起来不方便,重载一下
T get(int i) const
{
T ret;
if (get(i, ret))
{
return ret;
}
else
{
THROW_EXCEPTION(IndexOutOfBoundsException, "Invalid parameter i to get element...");
}
}
// 获取指定位置的元素
bool get(int i, T& e) const
{
bool ret = ((i>=0) && (i<m_length));
if (ret)
{
Node* current = &m_header;
for(int p = 0; p < i; p++)
{
current = current->next;
}
e = current->next->value;
// get是const成员函数,按理来说不能修改成员变量的值,Node* current=&m_header,会被误认为要更改成员变量的值,故报错
// 解决方案是对m_header加上mutable,开一个例外
}
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();
}
};
问题:头结点隐患,实现代码优化
创建m_header
时,会调用T value
,用泛指类型创建头结点的数据域,当泛指类型为用户自定义类型时,用用户自定义的类类型在库中创建对象,就有可能出错了,而且在外部看来,并没有用该类型创建对象,问题定位很麻烦。
解决办法:构造头结点时,不调用泛指类型创建头结点,而是按内存分布自己重建构造一个类对象,注意一定要和以前的头结点的内存分布一样,不仅是成员变量的内存大小,同样也要和以前一样继承于Object
// 直接创建头结点,存在隐患
mutable Node m_header;
// 重新构造之后的头结点
mutable struct : public Object
{
char reserved[sizeof(T)]; // 没实际作用,占空间
Node* next;
} m_header;
重新构造后的头结点在内存布局上和之前没有差异,差异在于不管泛指类型是什么,都不会去调用泛指类型的构造函数了。虽然它们在内存布局上是一样的,但是新构造的头结点是个空类型,不能直接用,使用时要进行类型转换
Node* ret = reinterpret_cast<Node*>(&m_header);
优化后的完整代码:
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)
{
// 注意i的范围
bool ret = ((i>=0) && (i<=m_length));
cout << "ret = " << ret << endl;
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)
{
// 注意i的范围
bool ret = ((i>=0) && (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 = ((i>=0) && (i<m_length));
if (ret)
{
position(i)->next->value = e;
}
return ret;
}
// get函数用起来不方便,重载一下
T get(int i) const
{
T ret;
if (get(i, ret))
{
return ret;
}
else
{
THROW_EXCEPTION(IndexOutOfBoundsException, "Invalid parameter i to get element...");
}
}
bool get(int i, T& e) const
{
bool ret = ((i>=0) && (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();
}
};
注意每次代码修改之后都要进行测试,有可能由于修改的代码引入了新的bug
3、小结
通过类模板实现链表,包含头结点和长度成员
定义结点类型,并通过堆中的结点对象构成链式存储
为了避免构造错误的隐患,头结点类型需要重定义
代码优化是编码完成后必不可少的环节