有一种很常用的数据结构叫链表,他简直就是孙悟空,七十二变还觉得少,单向链表,单向循环链表,双向链表,双向循环链表。。。。而且还有带头结点的、不带头节点的。果然是纷繁复杂的世界,不管怎样,先从最简单的开始。
一张粗图胜过千言万语,用图解的方式讲解数据结构是最好的方法。下图就是一个带头结点的单向链表。
创建一个带头结点的链表很简单,结点使用结构体定义,里面放数据元素和一个指向后继结点的指针,但是创建好的链表好像没啥用,什么操作都没有。没有那就加上嘛,增删查改是最基本的操作,现在就来逐个实现咯。
用图来说明单向链表的插入操作,简单明了啊!
具体用例子说明,把前驱结点1的next指针赋值为要插入的结点X,当前结点X的next指针就指向2结点。就这样完事了。
插入结点完成了,那就到删除结点了。还是拿图来说明吧,一看就懂。
总的来说就两步操作。
查找操作就没有什么图解的说法,直接循环比较就是了。但有一点需要特别强调的是,如果对单链表的遍历每次都从头节点出发,效率就会大大降低,特别是在频繁查找和获取某一结点的数据元素的场合,更是如此。所以,提供一组遍历函数,使用游标的方式来遍历链表。以后链表的遍历就很简单了。
上代码:
#ifndef LINKLIST_H
#define LINKLIST_H
template <typename T>
class LinkList
{
protected:
struct Node
{
T data;
Node* next;
};
int m_length;
int m_step; // 使用游标遍历的步长
mutable Node header; // 头节点,使用mutable声明兼容const对象
Node* m_current; // 游标
LinkList(const LinkList<T>& obj); // 作为容器类使用
LinkList<T>& operator = (const LinkList<T>& obj);
public:
LinkList() // 初始化工作要做好
{
header.next = NULL;
m_current = NULL;
m_length = 0;
m_step = 0;
}
bool insert(const T& value) // 尾插法,直接插入链表的尾部
{
return insert(m_length,value);
}
bool insert(int i, const T& value)
{
bool ret = (0 <= i) && (i <= m_length);
if( ret )
{
Node* n = new Node();
n->data = value;
Node* current = &header; // 获取前驱结点
for(int j = 0; j < i; ++ j)
{
current = current->next;
}
if( current == &header ) // 头插
{
n->next = header.next; // 两步操作
header.next = n;
}
else
{
n->next = current->next;// 两步操作
current->next = n;
}
++m_length;
}
else
{
throw(0);
}
return ret;
}
bool remove(int i)
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* current = &header;
for(int j = 0; j < i; ++ j) // 找到要删除的结点的前驱结点
{
current = current->next;
}
Node* todel = current->next; // 要删除的结点
if( m_current == todel ) /如果游标正好处在被删除结点的位置,则移动游标
{
m_current = m_current->next;
}
current->next = todel->next; // 指向要删除结点的后继结点
--m_length;
delete todel;
}
else
{
throw(0);
}
return ret;
}
int find(const T& obj) const
{
int ret = -1;
int i = 0;
Node* current = &header;
while( current != NULL ) // 从头到尾遍历
{
if( current->data== obj )
{
ret = i;
break; // 找到第一个就返回
}
++i;
current = current->next;
}
return ret;
}
bool set(int i, const T& obj)
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* current = header.next; // 找当前结点
for(int j = 0; j < i; ++ j)
{
current = current->next;
}
current->data = obj;
}
else
{
throw(0);
}
return ret;
}
T get(int i) const
{
T ret;
if( !get(i, ret) ) // 获取失败,抛出异常
{
throw(0);
}
return ret;
}
bool get(int i , T& value) const
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* current = header.next;
for(int j = 0; j < i; ++ j)
{
current = current->next;
}
value = current->data;
}
return ret;
}
int length() const
{
return m_length;
}
// 提供遍历函数,使用方便并且很高效;
bool move(int i, int step = 1) // 默认步长为1
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
m_current = header.next;
for(int j = 0; j < i; ++ j)
{
m_current = m_current->next;
}
m_step = step;
}
return ret;
}
bool end() // 判断是否到达链表尾部
{
return (m_current == NULL);
}
bool next() // 按步长移动游标
{
int i = 0;
while( (i < m_step) && !end() )
{
m_current = m_current->next;
++i;
}
return (i == m_step);
}
T current() // 返回游标当前位置的数据元素
{
if( !end() )
{
return m_current->data;
}
}
void clear() // 清空链表的操作
{
while( m_length > 0 )
{
remove(0);
}
}
~LinkList()
{
clear();
}
};
#endif
好长啊!先来个小测试。
#include "LinkList.h"
using namespace std;
int main(int argc, const char* argv[])
{
LinkList<int> list;
for(int i = 0; i < 10; ++ i)
{
list.insert(i); // 采用尾插,顺序是从小到大
}
for(list.move(0); !list.end(); list.next())
{
cout << list.current() << endl;
}
return 0;
}
单链表很好用,也经常使用,不过它没有什么特别。如果把最后一个结点的指针指向第一个结点,那么就得到一个环,这个环可以用来解决一个很著名的问题,约瑟夫环,故事的开端很有趣,某一天,历史学家约瑟夫和他的朋友为了躲避战乱,逃到一个山洞,山洞里有39个犹太人,但这些犹太人决定宁愿死也不要被敌人抓到,于是想出了一个可怕的自杀方式,41个人围成一个圈圈,由第1个人开始报数,每报数到第3人该人就必须自杀,然后再由下一个重新报数,直到所有人都自杀身亡为止。然而约瑟夫和他朋友可不想死啊,但又不敢反抗,毕竟人家人多势众!那只能靠智商了,只要找出最后自杀的两个位置就可以避免惨剧发生了。靠智商来救命,可能绝大部分人都会挂。然鹅,使用单向循环链表就可以轻松解决这个问题,保住性命!
现在就来实现循环的单链表。看图码字。
#ifndef CIRCLELIST_H
#define CIRCLELIST_H
template <typename T>
class CircleList
{
protected:
struct Node
{
T data;
Node* next;
};
int m_length;
int m_step;
Node* m_current;
mutable Node header;
public:
CircleList()
{
header.next = NULL;
m_current = NULL;
m_length = 0;
m_step = 0;
}
// 与单向链表相比,仅有插入、查找和删除操作的实现略微不同
// 其他函数基本一致,为节省篇幅,其他函数只写声明
bool move(int i, int step = 1);
bool next();
bool end(); // 由于是循环链表,可以无限循环,因此没有结尾,end函数可以省略
bool set(int i, const T& obj);
T get(int i) const;
bool get(int i, T& value) const;
int length() const;
void clear();
// 重新实现插入、删除、查找操作
bool insert(const T& obj)
{
return insert(m_length, obj);
}
bool insert(int i, const T& obj)
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* n = new Node();
n->data = obj;
Node* current = &header;
for(int j = 0; j < i; ++ j)
{
current = current->next;
}
// 如果是头插,需要注意分类讨论
if( current == &header )
{
if( current->next == NULL )// 空链表
{
current->next = n;
n->next = n; // 独自成环
}
else
{
Node* tail = &header; // 得到尾结点
for(int j = 0; j < m_length; ++ j)
{
tail = tail->next;
}
n->next = current->next;
current->next = n;
tail->next = n; // 首尾相连
}
}
else
{
n->next = current->next;
current->next = n;
}
++m_length;
}
else
{
throw(0);
}
return ret;
}
bool remove(int i)
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* current = &header;
for(int j = 0; j < i; ++ j)
{
current = current->next;
}
Node* todel = current->next;
// 如果游标正好位于被删除结点位置,则移动游标
if( m_current == todel && (m_length > 1) )
{
m_current = m_current->next;
}
// 删除头节点同样需要注意分情况
if( current == &header )
{
if( 1 == m_length )
{
current->next = NULL;
m_current = NULL:
}
else
{
Node* tail = &header;// 获取尾结点
for(int j = 0; j < m_length; ++ j)
{
tail = tail->next;
}
current->next = todel->next;
tail->next = todel->next;// 保持环状
}
}
else
{
current->next = todel->next;
}
--m_length; // 先后顺序考虑
delete todel; // 异常安全问题
}
else
{
throw(0);
}
return ret;
}
int find(const T& value) const
{
int ret = -1;
Node* current = header.next;
for(int i = 0; i < m_length; ++ i)
{
if( current->data == value )
{
ret = i;
break;
}
current = current->next;
}
return ret;
}
};
#endif
现在就可以用单向循环链表来搞定约瑟夫环。好可怕的游戏!!
#include <iostream>
#include "CircleList.h"
using namespace std;
// 参数n为参与人数 ,s 为第一个开始的人 , m 为数到的那个倒霉鬼
void josephus(int n, int s, int m)
{
CircleList<int> cl;
// 人数从 1 开始
for(int i = 1; i <= n; ++ i)
{
cl.insert(i);
}
cl.move((s-1,m-1);// 链表下标从 0 开始,所以要相应减一
while( cl.length() > 0 )
{
cl.next();
cout << "Suicided person : " << cl.current() << endl;
cl.remove(cl.find(cl.current()));// 自杀了,就离开这个圈圈
}
}
int main(int argc, const char* argv[])
{
josephus(41, 1, 3); // 41个人, 从第一个开始, 数到 3 就自杀
return 0;
}
约瑟夫和他的朋友站在16和31的位置那就平安大吉了。不得不感叹,数学不好,真的会s。
一鼓作气搞定双向链表以及双向循环链表,有了单向链表和单向循环链表的基础,实现起来就很轻松了。但需要强调再强调的是前驱和后继指针一定要小心赋值的顺序,指针操作不当很容易就导致整个链表状态出错。
看看图就马上能理解双向链表以及双向循环链表了。
双向循环链表确实的,有点点复杂,不过办法总比困难多!
把图放在心中,注意细节,目标代码就很清晰了。双向链表可以复用单向链表中的绝大部分代码,双向循环链表也是可以复用单向循环链表中的绝大部分代码,所以掌握了单向的,实现双向的就轻而易举了。上代码:
#ifndef DUALLINKLIST_H
#define DUALLINKLIST_H
template <typename T>
class DualLinkList
{
protected:
struct Node
{
T data;
Node* next;
Node* pre; // 增加前驱指针
};
int m_length;
int m_step;
Node* m_current;
mutable Node header;
public:
DualLinkList()
{
m_length = 0;
m_step = 0;
m_current = NULL;
header.next = NULL;
header.pre = NULL;
}
// 可以复用单向链表的函数就仅仅写声明,具体实现参照上述的单向链表
bool set(int i, const T& value);
bool get(int i) const;
bool get(int i, T& value) const;
bool move(int i, int step = 1);
bool next();
bool end();
bool insert(const T& obj);
int find(const T& value);
void clear();
bool insert(int i, const T& obj)
{
bool ret = (0 <= i) && (i <= m_length);
if( ret )
{
Node* n = new Node();
n->data = obj;
Node* current = &header; // 找前驱结点
for(int j = 0; j < i; ++j)
{
current = current->next;
}
Node* next = curret->next; // 得到后继结点
if( current == &header ) // 头插
{
n->pre = NULL;
n->next = next;
if( next != NULL ) // 如果不是空链表
{
next->pre = n;
}
current->next = n;
}
else
{
current->next = n; // 四步操作
n->pre = current;
n->next = next;
if( next != NULL )
{
next->pre = n;
}
}
++m_length;
}
else
{
throw(0);
}
return ret;
}
bool remove()
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* current = &header;
for(int j = 0; j < i; ++ j)
{
current = current->next;
}
Node* todel = current->next;
Node* next =todel->next;
if( m_current == todel ) // 注意游标位置
{
m_current = m_current-next;
}
if( current == &header ) // 头删
{
current->next = next;
if( next !=- NULL )
{
next->pre = NULL;
}
}
else
{
current->next = next;
next->pre = current;
}
--m_length; // 考虑异常安全
delete todel;
}
else
{
throw(0);
}
}
bool pre() // 双向链表,提供前向遍历,与后向遍历相呼应
{
int i = 0;
while( (i < m_step) && !end() )
{
m_current = m_current->pre;
i++;
}
return (i == m_step);
}
};
#endif
搞定一个双向链表啦,赶紧试试一下功能正常不。
#include <iostream>
#include "DualLinkList.h"
using namespace std;
int main(int argc, const char* argv[])
{
DualLinkList<int> dl;
for(int i = 0; i < 10; ++ i)
{
dl.insert(i);
}
cout << " length = " << dl.length() << endl;
for(dl.move(0)); !dl.end(); dl.next() // 正向遍历
{
cout << dl.current() << " ";
}
for(dl.move(dl.length()-1); !dl.end(); dl.pre()) // 逆向遍历
{
cout << dl.current() << " ";
}
return 0;
}
搞完最后一个双向循环链表就可以收工,加油!
#ifndef DUALCIRCLELIST_H
#define DUALCIRCLELIST_H
template <typename T>
class DualCircleList
{
protected:
strcut Node
{
T data;
Node* next;
Node* pre;
}
int m_length;
int m_step;
Node* m_current;
mutable Node header;
public:
DualCircleList()
{
m_length = 0;
m_step = 0;
m_current = NULL;
header.next = NULL;
header.pre = NULL;
}
// 除了插入和删除操作与众不同,其他完全可以复用单向链表和单向循环链表
// 以及双向链表中的相关功能函数
bool insert(const T& obj)
{
return insert(m_length, obj);
}
bool insert(int i, const T& obj)
{
bool ret = (0 <= i) && (i <= m_length);
if( ret )
{
Node* n = new Node();
n->data = obj;
Node* current = &header;
for(int j = 0; j < i; ++ j)
{
current = current->next;
}
Node* next = current->next;
if( current == &header ) // 头插
{
if( current->next == NULL ) // 空链表,独自成环
{
current->next = n;
n->next = n;
n->pre = n;
}
else
{
Node* tail = &header; // 获取尾结点
for(int j = 0; j < m_lengh; ++ j)
{
tail = tail->next;
}
current->next = n; // 连接结点
n->next = next;
n->pre = tail;
next->pre = n;
tail->next = n;
}
}
else
{
current->next = n;
n->next = next;
n->pre = current;
next->pre = n;
}
++m_length;
}
else
{
throw(0);
}
return ret;
}
bool remove(int i)
{
bool ret = (0 <= i) && (i < m_length);
if( ret )
{
Node* current = &header; // 得到前驱结点
for(int j = 0; j < i; ++ j)
{
current = current->next;
}
Node* todel = current->next;
Node* next = todel->next;
if( m_current == todel && m_length > 1 ) // 注意游标的位置是否与被删除的结点相同
{
m_current = m_current->next;
}
if( current == &header ) // 头删
{
if( m_length == 1 ) // 一个元素
{
m_current = NULL;
current->next = NULL;
}
else
{
Node* tail = &header; // 得到尾结点
for(int j = 0; j < m_length; ++ j)
{
tail = tail->next;
}
current->next = next;
tail->next = next;
if( tail == next ) // 两个元素的情况,
{
tail->pre = next;
}
else // 还有多个元素的情况
{
next->pre = tail;
}
}
}
else
{
current->next = next;
next->pre = current;
}
--m_length;
delete todel;
}
else
{
throw(0);
}
return ret;
}
};
#endif
测试代码可以直接修改单向循环链表解决约瑟夫环的程序。