之前的文章中讲过了动态数组的实现,但是分析得知动态数组无论如何巧妙,增删复杂度最差都是 o(n) ,如果我们有这样一个使用场景,对于一个停车场,经常有人进出,所以需要频繁修改数据。如果停车场确实数量少,使用数组还可以,但是数量多了,肯定不可以使用数组了,因为比较慢。(ps:当然有更好的选择,后续介绍AVL树和RB树时再介绍)
链表的结构 那么链表是一个什么样的结构呢?代码如下
templateclass 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;inext;
return p;
}
可以看到找到一个节点,需要进行n次遍历 ,时间复杂度为 o(n)。相较于vector支持随机访问,非常慢。所以我们应该选择正确的结构。我们可以对比一下两种基础结构的区别,打好基础是最重要的,如果基础不牢,后面学习一些复杂结构就会非常困难。
| vector | list | |
|---|---|---|
| 添加 | o(n),遍历 | o(1),结构调整 |
| 删除 | o(n),遍历 | o(1),结构调整 |
| 修改 | o(1),支持随机访问 | o(n),遍历 |
| 访问 | o(1),支持随机访问 | o(n),遍历 |
这个链表是一个双向链表,其实这个我还想着作为一个轮子用,但是确实写的时候遇到很多问题,比如如何支持类对象,对于基础类型如何做优化。因为支持类对象就设计构造析构,或许出现用户设计的类不完善的情况。这些情况可能在后续有优化。
因为调整结构非常频繁,所以直接写个可以复用的接口
template链表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; } };
templateclass List { public: List() { init(); } //默认构造 List(const List&); //拷贝构造 List & operator=(const List&); //拷贝构造运算符 ~List() { clear(); }; public: bool empty() { return !_size; } //判空 size_t size() { return _size; } //大小 virtual void init(); //初始化基础资源 virtual void clear(); //释放全部动态资源,回归初始状态 //遍历器 void traverse() { ListNodePtr tmp = _head->_next; while (tmp->_next != nullptr) { func(*tmp); tmp = tmp->_next; } }; //传入遍历器进行遍历 //增删改查 virtual ListNodePtr insert(size_t index,const T&& data); //根据指定下标,在节点元素后,插入该目标元素 virtual ListNodePtr 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 )) { func = p; } //设置遍历器 private: //遍历器 std::function )> func; ListNodePtr _head; //头哨兵 ListNodePtr _tail; //尾哨兵 size_t _size; };
只介绍重要接口
-
遍历器:可以用function或者函数指针。初始化遍历器,然后调用traverse,就可以遍历每个元素。STL中有算法库,所以STL很方便;
-
插入操作:指定位置后面插入元素;
template
ListNodePtr List ::insert(size_t index,const T&& data) { ++_size; //修改元素数量 return this->operator[](index-1)->changeNext(new ListNode (data)); //这里下标-1传入和下标运算符策略有关。 } -
删除:支持范围删除。支持删除元素自动释放;
template
void List ::earse(size_t start, size_t end) { ListNodePtr p = this->operator[](start)->_prev; ListNodePtr 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) 的下标访问。
template
ListNodePtr List ::operator[](size_t index) { ListNodePtr 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



