List的泛型实现
一、介绍链表
之前的文章中讲过了动态数组的实现,但是分析得知动态数组无论如何巧妙,增删复杂度最差都是 o(n) ,如果我们有这样一个使用场景,对于一个停车场,经常有人进出,所以需要频繁修改数据。如果停车场确实数量少,使用数组还可以,但是数量多了,肯定不可以使用数组了,因为比较慢。(ps:当然有更好的选择,后续介绍AVL树和RB树时再介绍)
链表的结构
那么链表是一个什么样的结构呢?代码如下
/*可以用泛型实现,也可以特化
一个节点中包含:
1、一个指向下一个节点的指针
2、一个数据元素
*/
template<typename T>
class listnode
{
listnode* next;
//T data;
T* data;
};
可以参考下面的图片,链表基础模型就是这样的结构
链表的特点
链表使用指针的特点让它可以很快的调整结构,因为只需要改变指针指向就可以了。
//如何删除 p 的下一个节点
void earse(listnode* p)
{
//如果是动态内存,需要释放
//auto tmp = p->next;
p->next = p->next->next; //让p节点指向p节点的下一个的下一个节点
//delete tmp;
}
//如何添加数据元素
void insert(listnode* p,listnode* newnode)
{
newnode->next = p->next; //让新节点,指向p的下一个节点
p->next = newnode; //让p指向新节点
}
链表的缺点
优点明显,当然缺点也很明显,如果我们想在一个链表中找的某一个位置的元素怎么办?
listnode& find(listnode* p , size_t n)
{
//不负责溢出检测,秉承着溢出也是为了客户体验,且让客户享受到原汁原味的溢出体验
for(int i=0;i<n;++i)
p = p->next;
return p;
}
可以看到找到一个节点,需要进行n次遍历 ,时间复杂度为 o(n)。相较于vector支持随机访问,非常慢。所以我们应该选择正确的结构。我们可以对比一下两种基础结构的区别,打好基础是最重要的,如果基础不牢,后面学习一些复杂结构就会非常困难。
vector | list | |
---|---|---|
添加 | o(n),遍历 | o(1),结构调整 |
删除 | o(n),遍历 | o(1),结构调整 |
修改 | o(1),支持随机访问 | o(n),遍历 |
访问 | o(1),支持随机访问 | o(n),遍历 |
二、泛型实现的list
结点
这个链表是一个双向链表,其实这个我还想着作为一个轮子用,但是确实写的时候遇到很多问题,比如如何支持类对象,对于基础类型如何做优化。因为支持类对象就设计构造析构,或许出现用户设计的类不完善的情况。这些情况可能在后续有优化。
因为调整结构非常频繁,所以直接写个可以复用的接口
template<typename T>
struct ListNode
{
ListNode(T data = 0, ListNode* p = nullptr, ListNode* n = nullptr) : _data(data), _prev(p), _next(n) {}; //构造函数
ListNode* _prev; //前驱
ListNode* _next; //后继
T _data; //数据成员
//修改前驱为新前驱,自动调整节点关系(也就是说,对于前驱后继都不匹配的,都可以指定当前位置正确的前驱后继关系)
ListNode* changePrev(ListNode* newPrev)
{
//newPrev作为新前驱
newPrev->next = this;
newPrev->prev = this->prev;
this->prev->next = newPrev;
this->prev = newPrev;
return newPrev;
}
//修改后继为新后继,自动调整节点关系
ListNode* changeNext(ListNode* newNext)
{
newNext->_next = this->_next;
newNext->_prev = this;
this->_next->_prev = newNext;
this->_next = newNext;
return newNext;
}
};
链表
template<typename T>
class List
{
public:
List() { init(); } //默认构造
List(const List&); //拷贝构造
List<T>& operator=(const List&); //拷贝构造运算符
~List() { clear(); };
public:
bool empty() { return !_size; } //判空
size_t size() { return _size; } //大小
virtual void init(); //初始化基础资源
virtual void clear(); //释放全部动态资源,回归初始状态
//遍历器
void traverse() {
ListNodePtr<T> tmp = _head->_next;
while (tmp->_next != nullptr) {
func(*tmp);
tmp = tmp->_next;
}
}; //传入遍历器进行遍历
//增删改查
virtual ListNodePtr<T> insert(size_t index,const T&& data); //根据指定下标,在节点元素后,插入该目标元素
virtual ListNodePtr<T> operator[](size_t index); //访问运算符
virtual void earse(size_t index) { earse(index, index); }; //特化
virtual void earse(size_t start,size_t end); //泛化:删除元素
virtual void setFunction(void p(ListNode<T>)) { func = p; } //设置遍历器
private:
//遍历器
std::function<void(ListNode<T>)> func;
/* 类似迭代器操作,方便标识边界 */
ListNodePtr<T> _head; //头哨兵
ListNodePtr<T> _tail; //尾哨兵
/* 维护一个size是有意义的,大多数情况下可以减少遍历次数 */
size_t _size;
};
只介绍重要接口
-
遍历器:可以用function或者函数指针。初始化遍历器,然后调用traverse,就可以遍历每个元素。STL中有算法库,所以STL很方便;
-
插入操作:指定位置后面插入元素;
template<typename T> ListNodePtr<T> List<T>::insert(size_t index,const T&& data) { ++_size; //修改元素数量 return this->operator[](index-1)->changeNext(new ListNode<T>(data)); //这里下标-1传入和下标运算符策略有关。 }
-
删除:支持范围删除。支持删除元素自动释放;
template<typename T> void List<T>::earse(size_t start, size_t end) { ListNodePtr<T> p = this->operator[](start)->_prev; ListNodePtr<T> tmp; for (int i = 0; i < (end - start)+1; ++i) { tmp = p->_next; tmp->_next->_prev = p; p->_next = tmp->_next; delete tmp; p = p->_next; } _size -= end-start+1; }
-
重载了下标运算符,支持 o(n) 的下标访问。
/* 复杂度分析 : 因为是遍历,故时间复杂度为 o(n); 使用一个辅助空间,空间复杂度为 o(1) */ template<typename T> ListNodePtr<T> List<T>::operator[](size_t index) { ListNodePtr<T> tmp = _head; for (int i = 0; i < index+1; ++i) { tmp = tmp->_next; } return tmp; }
学习中造的轮子,以后有机会还会继续完善,目前属于是能使用。可以直接继承并直接重写,后续会介绍我在httpserver中使用这个轮子做的定时器队列。后续想要提升轮子的泛型能力,就需要深入模板技术了。
三、github:
如果有后续,我会持续更新我的轮子。
}
return tmp;
}
学习中造的轮子,以后有机会还会继续完善,目前属于是能使用。可以直接继承并直接重写,后续会介绍我在httpserver中使用这个轮子做的定时器队列。后续想要提升轮子的泛型能力,就需要深入模板技术了。
### 三、github:[](https://github.com/yqm-307/yqm_data_structure)
如果有后续,我会持续更新我的轮子。
博主是正在学习中的大三学生。C++内卷群:672372860