数组是软件开发过程当中很是重要的一种数据结构,可是数组至少有两个局限:算法
一、单向链表数组
事实上,咱们更加关注的是基于数据结构的算法,链表是一种简单的数据组织方式,适合中等数量的数据,咱们考察链表的添加、删除、查找便可,更加复杂的操做需求最好使用更加高级的数据结构。数据结构
首先定义链表:函数
#ifndef INT_LINKED_LIST
#define INT_LINKED_LIST
class Node {
public:
//构造函数,建立一个节点
Node(int el = 0, Node* ptr = nullptr) {
info = el;
next = ptr;
}
//节点的值
int info;
//下一个节点地址
Node* next;
};
class NodeList {
public:
//构造函数,建立一个链表,用于管理节点
NodeList() {
head = tail = nullptr;
}
//节点插入到头部
void addToHead(int);
//节点插入到尾部
void addToTail(int);
//删除头部节点
int deleteFromHead();
//删除尾部节点
int deleteFromTail();
//删除指定节点
void deleteNode(int);
private:
//头指针、尾指针
Node *head, *tail;
};
#endif
复制代码
这里定义了两个class
,分别用来表示节点以及管理节点的链表。其中,节点具备两个成员变量,分别是当前节点的值以及指向下个节点的指针。链表也具备两个成员变量,分别指向头结点以及尾节点。链表class
具备5个成员方法,分别表明着节点的添加、删除、查找,咱们来考察下这3种操做在链表中的表现。post
单向链表的操做比较简单,这里直接使用动图来代替代码,更加易于理解。学习
假设已有链表以下:优化
节点插入到头部的逻辑比较简单,算法复杂度能在固定时间O(1)
内完成,也就是说,不管链表中有多少个节点,该函数所执行操做的数目都不会超过某个常数c。注意,该操做的实现依赖head
指针,不然没法肯定头结点的地址,那么算法的复杂度将会大大增长。spa
节点插入到尾部的逻辑和插入到头部类似,算法复杂度也是O(1)
,区别在于该操做的实现依赖tail
指针,不然没法肯定尾节点的地址,那么算法的复杂度将会大大增长。设计
删除头部节点操做的算法复杂度也是O(1)
,该操做依赖head
指针,经过head
指针能够直接获取到下个节点的地址,因此复杂度很低。指针
注意这里,删除尾部节点的算法复杂度是O(n)
,相比于前面的O(1)
,提高了两个量级。缘由在于咱们须要一个临时指针p,从头结点一直遍历到倒数第二个节点。由于删除尾节点以后,tail指针须要向头结点方向移动一次,可是在链表中不能直接获取到倒数第二个节点的地址,只能依靠遍历的方式,这就致使算法复杂度上升为O(n)
。在单向链表中没有更好的解决方式了,在后面咱们须要改进链表结构避免这种状况。
删除指定节点的算法复杂度也是不尽人意,在最好的状况下花费O(1)
的时间,在最坏和平均状况下则是O(n)
。经过动态图能够发现,咱们定义P指针指向目标节点,定义Q节点指向目标节点的前驱节点。这两个变量的存在乎义在于修正单向链表的指向,是不可或缺的。
基于单向链表的某些操做的算法复杂度没法知足咱们的需求,这里主要指删除尾部节点以及删除指定节点,它们的平均复杂度达到了O(n)
,相比于O(1)增长了两个量级。为了改进算法,咱们须要修改链表的结构。对于删除尾部节点来讲,瓶颈在于没法直接获取尾节点的前驱节点地址,咱们能够为节点加上一个指向前节点的指针来解决,这就是所谓的双向链表。
二、双向链表
双向链表是这个样子:
首先是定义:
#ifndef INT_LINKED_LIST
#define INT_LINKED_LIST
class Node {
public:
//构造函数,建立一个节点
Node(int el = 0, Node* p = nullptr, Node* q = nullptr) {
info = el;
pre = p;
next = q;
}
//节点的值
int info;
//前一个节点地址
Node* pre;
//下一个节点地址
Node* next;
};
class NodeList {
public:
//构造函数,建立一个链表,用于管理节点
NodeList() {
head = tail = nullptr;
}
//节点插入到头部
void addToHead(int);
//节点插入到尾部
void addToTail(int);
//删除头部节点
int deleteFromHead();
//删除尾部节点
int deleteFromTail();
//删除指定节点
void deleteNode(int);
private:
//头指针、尾指针
Node *head, *tail;
};
#endif复制代码
基于双向链表的操做和单向链表很是类似,咱们是从单向链表中扩展出双向链表的,目的是改进删除尾部节点的算法。
能够看到删除尾部节点的算法复杂度已经降至O(1)
,事实上pre
指针不只仅简化了删除尾节点操做,对于其余O(1)
的操做也有简化,由于有了pre
指针,有些临时指针就不必定义了。
尽管如此,咱们仍是增长了空间的使用程度才下降了时间上的消耗,本质上是空间换取时间的作法。对于现代软件开发来说,硬件已经不是主要瓶颈,一些空间上的代价是值得的。
也许有人了解过所谓的循环单向链表、循环双向链表,它们究竟是什么东西呢?
循环单向链表和单向链表的差异:
差异就在于尾节点的next指针循环指向了头结点,这时候head指针就不必存在了,若是继续定义head指针,只是更加方便一些,但它已经不是不可或缺的了。
循环双向链表和双向链表的差异:
一样的道理,head
指针根据须要添加。循环链表和普通链表没有本质的差异,能够根据须要自行选择。
到目前为止,咱们还有一个问题没有解决,那就是删除指定节点。该操做本质上是查找问题,为了优化查找算法,咱们须要继续对链表结构进行改动。事实上,上述链表已经足够知足需求了,由于咱们假设对象是中等数量的数据,O(n)
级别的操做能够接受,对于更加复杂的数据,须要更加复杂的数据结构进行处理。出于学习的态度,能够继续研究,毕竟有句话叫作-厚积薄发。