基本概念和特点
链表的定义
-线性表的链式存储结构称之为链表(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