关于链表(LinkedList)的定义,这里我就不作解释了,如果你还不知道什么是链表,那么本文并不适合你阅读。同时本文使用C++实现,如果你还不懂C++的基础语法,那么本文也不适合你阅读。今天我以单链表为例教你实现一个链表。小编是一名大二学生,学习C++也不过几个月的时间,如果有写错的地方,还望斧正!
接口 | 功能 |
---|---|
add | 添加 |
insert | 插入 |
remove | 删除 |
clear | 清空 |
set | 修改 |
get | 按索引获取 |
indexOf | 搜索 |
forEach | 遍历 |
在C++中接口是通过抽象类是实现的,因此我们定义一个类,名为AbstractList,同时将方法添加进去
template<typename T>
class AbstractList {
public:
virtual ~AbstractList() {
}
public:
// 增
virtual AbstractList<T>& add(T elem) = 0;
virtual AbstractList<T>& insert(T newElem, size_t pos) = 0;
// 删
virtual AbstractList<T>& remove(size_t pos) = 0;
virtual AbstractList<T>& clear() = 0;
// 改
virtual AbstractList<T>& set(size_t index, T newElem) = 0;
// 查
virtual const T& get(size_t index) const = 0;
virtual size_t indexOf(T target) const = 0;
// 遍历
virtual AbstractList<T>& forEach(const std::function<void(T)>& fn) = 0;
};
首先定义节点类,名为LinkedListNode,该类可以通过struct定义在LinkedList的内部,也可以像我一样使用class单独定义为一个类,为了方便我将LinkedList声明为LinkedListNode的友元类,这样LinkedList就能访问到该类的所有属性了,不过在声明友元之前,LinkedList尚未编译,所以要先声明LinkedList。同时为了检测内存泄漏,我在LinkedListNode中定义了一个静态属性 CNT,这个属性在构造函数中+1,在析构函数中-1。如果确认内存不泄漏,可以将该属性删除。
// 前向声明
template<typename T>
class LinkedList;
template<typename T>
class LinkedListNode {
friend class LinkedList<T>;
public:
// CNT 用来检查内存泄漏
static int CNT;
private:
T _value;// 数据域
LinkedListNode<T>* next; //后继
public:
LinkedListNode(T value, LinkedListNode<T>* next) : _value(value), next(next) {
CNT++;
}
virtual ~LinkedListNode() {
CNT--;
}
};
template<typename T>
int LinkedListNode<T>::CNT = 0;
首先 公有继承AbstractList类 ,将接口导入进来,该类的属性有_head,_tail,_length,均为私有属性,_head是头结点指针,_tail是尾结点指针,_length是链表的长度。该类需要实现拷贝构造函数、需要重载operator=,将默认的拷贝构造函数和默认的赋值运算符都改为深拷贝,并且需要实现析构函数来释放new出来的内存。
template<typename T>
class LinkedList: public AbstractList<T>{
private:
LinkedListNode<T>* _head = nullptr;
LinkedListNode<T>* _tail = nullptr;
size_t _length = 0;
public:
LinkedList<T>& add(T elem) override {
}
LinkedList<T>& insert(T newElem, size_t pos) override {
}
LinkedList<T>& remove(size_t pos) override {
}
LinkedList<T>& clear() override {
}
LinkedList<T>& set(size_t index, T newElem) override {
}
const T& get(size_t index) const override {
}
size_t indexOf(T target) const override {
}
LinkedList<T>& forEach(std::function<void(T)> fn) override{
}
};
/**
* 尾插
* @param data 新的数据
* @return 自身
*/
LinkedList<T>& add(T data) override {
// 生成结点
LinkedListNode<T>* pNode = new LinkedListNode<T>(data, nullptr);
// 如果是空链表
if (!_length) {
// 如果头指针不为空,则先把头指针内存释放
if (_head != nullptr)delete _head;
// 头指针指向新的节点
_head = new LinkedListNode<T>(data, pNode);
// 尾指针指向新的节点
_tail = pNode;
}
// 如果不是空链表
else {
// 尾指针后移
_tail->next = pNode;
// 尾指针指向新生成的结点
_tail = pNode;
}
// 长度+1
_length++;
// 返回自身,便于函数式编程
return *this;
}
/**
* 删除
* @param index要删除元素的索引
* @return 自身
*/
LinkedList<T>& remove(size_t index) override {
// 如果要删除元素的索引错误,则抛出异常
if (index > _length - 1) {
throw exception("remove:溢出!");
}
// 开始遍历链表,注意:这里要从从头指针开始遍历,因为要删除的元素的索引可能为0
int i = -1;
for (LinkedListNode<T>* ptr = _head; ptr; ptr = ptr->next,i++) {
// 找到目标元素的前驱结点
if (i == index - 1) {
// 先存一下目标结点
auto temp = ptr->next;
// 将前驱结点的后继改为目标结点的后继
ptr->next = ptr->next->next;
// 删除目标结点
delete temp;
// 如果删除的是尾结点
if (index == _length - 1) {
// 尾结点前移
_tail = ptr;
}
// 退出循环
break;
}
}
// 长度-1
_length--;
// 如果删除之后变为空链表
if (_length == 0) {
// 释放内存
delete _head;
//将头指针和尾指针置空
_head = _tail = nullptr;
}
return *this;
}
LinkedList<T>& clear() override {
// 如果是空链表,直接返回自身
if (!_length)return *this;
// 工作指针从头结点开始遍历链表
LinkedListNode<T>* ptr = _head;
while (ptr) {
// 临时存一下当前指向的节点
auto temp = ptr;
// 工作指针后移
ptr = ptr->next;
// 释放内存
delete temp;
}
// 头指针,尾指针置空
_head = _tail = nullptr;
// 长度置0
_length = 0;
// 返回自身
return *this;
}
/**
* 插入
* @param newElem 新的元素
* @param pos 要插入的位置
* @return 自身
*/
LinkedList<T>& insert(T newElem, size_t pos) override {
// 如果要插入的位置大于等于链表的长度,则直接尾插
if (pos >= _length) return add(newElem);
// 生成一个结点,后继待定
LinkedListNode<T>* pNode = new LinkedListNode<T>(newElem, nullptr);
// 从头指针开始遍历链表
int i = -1;
for (LinkedListNode<T>* ptr = _head; ptr; ptr = ptr->next,i++) {
// 找到目标结点的前一个结点
if (i == pos - 1) {
// 新结点的后继指向目标结点
pNode->next = ptr->next;
// 前一个结点的后继指向新节点
ptr->next = pNode;
// 退出循环
break;
}
}
// 长度+1
_length++;
// 返回自身
return *this;
}
/**
* 按值查找
* @param target 要查找的元素
* @return 该元素对应的索引
*/
size_t indexOf(T target) const override {
int i = 0;
// 遍历链表
for (LinkedListNode<T>* ptr = _head->next; ptr; ptr = ptr->next,i++) {
// 找到该元素,返回该元素
if (ptr->_value == target) {
return i;
}
}
// 未找到抛出异常
throw exception("find:未找到该元素!");
}
/**
* 修改
* @param index 要修改元素的索引
* @param newElem 新的值
* @return 自身
*/
LinkedList<T>& set(size_t index, T newElem) override {
// 如果索引不正确,抛出异常
if (index > _length - 1) {
throw exception("set:溢出!");
}
// 遍历链表
int i = 0;
for (LinkedListNode<T>* ptr = _head->next; ptr; ptr = ptr->next) {
// 找到要修改的元素
if (i++ == index) {
// 修改值
ptr->_value = newElem;
// 返回自身
return *this;
}
}
}
/**
* 搜索
* 通过该方法返回的值只读
* @param index 索引
* @return 该索引对应的数据
*/
const T& get(size_t index) const override {
if (_length == 0)throw exception("get:空链表!");
else if (index >= _length) throw exception("get:溢出!");
// 遍历链表,i用于计数
int i = 0;
for (LinkedListNode<T>* ptr = _head->next; ptr; ptr = ptr->next) {
// 找到对应的结点,就返回
if (i++ == index) {
return ptr->_value;
}
}
}
我们会发现get方法获取一个值就需要遍历一次链表,时间复杂度为O(n)。
此时我们想一下这种情况:
在外界如何打印该链表?
你可能会说利用int i 游标使用get方法遍历链表,源代码如下:
LinkedList<int> list;
list.add(1);
list.add(2);
list.add(3);
for (int i = 0; i < list.length(); ++i) {
cout << list.get(i) << "\t";
}
cout << endl;
是的,这确实没错,但是利用get效率实在太低了,这样的算法时间复杂度为O(n²),这怎么能忍?
于是我实现了forEach方法,这个方法可以从外接收一个lambda表达式或函数,这样就可以一边遍历一边调用传进来的函数,代码如下:
/**
* 遍历
* 1.该方法从外界接收一个 返回值为void 参数类型为T的lambda表达式或函数的常引用
* 传入的lambda表达式或函数的参数的传递方式采用值传递,这是为了避免在forEach遍历
* 过程中修改到表中的数据,同时也避免了两个(指针)引用指向同一对象
* 2.传常引用是为了提升效率
* @param fn lambda表达式或者函数的常引用
* @return 自身
*/
LinkedList<T>& forEach(const std::function<void(T)>& fn) override {
if (_length == 0)throw exception("forEach:空链表!");
for (LinkedListNode<T>* ptr = _head->next; ptr; ptr = ptr->next) {
fn(ptr->_value);
}
return *this;
}
lambda表达式是C++11的新特性 基本语法为:
[捕获列表](参数列表)->返回类型{函数体}
返回类型可以省略
std::function类是C++11新增的特性,在C++11之前你可以通过函数指针实现相同功能,但捕获列表不为空的lambda是不能通过函数指针实现的!
OK,实现forEach之后,遍历效率就高了许多了,你可以使用以下语句进行遍历
LinkedList<int> list;
list.add(1);
list.add(2);
list.add(3);
list.forEach([](int item) {
cout << item << '\t'; });
cout << endl;
这样看起来就舒服多了!
同时你还可以使用lambda捕获一下变量:
LinkedList<int> list, list1;
list.add(1);
list.add(2);
list.add(3);
// 将list中的全部数据加入list1
list.forEach([&](int item) {
list1.add(item); });
// 打印list1
list1.forEach([](int item) {
cout << item << "\t"; });// 输出 1,2,3
cout << endl;
在上面的代码中我将list中的代码加入到了list1中,由于lambda的参数采用值传递,你可以不用担心两个引用指向同一个对象这种问题。
size_t length() const {
return _length;
}
LinkedList(const LinkedList<T>& src) {
// 将src中的数据添加到新的链表
for (LinkedListNode<T>* ptr = src._head->next; ptr; ptr = ptr->next) {
add(ptr->_value);
}
}
LinkedList& operator=(const LinkedList<T>& src) {
// 判断自身复制
if (&src == this) {
return *this;
}
// 先清空
clear();
// 从src添加新的数据
for (LinkedListNode<T>* ptr = src._head->next; ptr; ptr = ptr->next) {
add(ptr->_value);
}
//返回自身的引用
return *this;
}
直接清空即可
~LinkedList() override {
clear(); }
OK,至此,这个单链表已经基本实现了,你还可以添加一些其他功能,例如:sort,reverse等等,你也可以选择重载一下其他运算符,你也可以想想跳跃表和普通链表有哪些优势,你还可以把这个类扩展为Stack或者Queue,使这个类更方便、快捷、好用,不要局限于书上的那些简单算法和数据结构,把思维打开才能获得更多知识!
我已经将代码提交到github上,大家可以下载:
https://github.com/Ozral/LinkedList