启迪思维:双向循环链表
文章目录:
01. 博文简介:
02. 概念:
03. 示例图:
04. 优缺点:
05. 代码分析:
06. 运行环境:
07. 题记:
一:概念
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点,一般我们都构造双向循环表,STL中实现了双链表循环表。
二:示例图
三:链表的优缺点
链表最大的一个缺点,就是查找一个元素,必须整个链表遍历,也可以看出这种数据结构便于插入或者删除一个接点。
四:代码分析
1、获取节点信息
1 /** 2 *根据位置获取相对应的节点信息 3 *size_t 是一个无符号的类型,确定调用该函数时入参一定是正数, 4 *这样的要求也提醒我们在写函数时,应该避免把这样函数暴露给客户 5 *不知道为什么linux很多api函数都有size_t类型的入参(通过调试,传入负数会导致严重的bug), 6 *有知道的朋友,请帮忙分享下 7 */ 8 DLNode<T>* GetElemP(size_t i) const{ 9 int j = 0; 10 DLNode<T> *p = head; 11 12 do{ 13 p = p->next; 14 j++; 15 }while(p != head && j < i); 16 17 return p; 18 } 19 20 /** 21 *根据元素获取对应的节点信息 22 */ 23 DLNode<T>* GetElemE(const T &e) const{ 24 DLNode<T> *p = head->next; 25 26 while(p != head && e != p->data){ 27 p = p->next; 28 } 29 30 return p; 31 }
2、插入节点
插入过程如下图:
代码分析如下:
1 /** 2 *在指定的位置插入节点 3 */ 4 void Insert(size_t i,const T &e){ 5 //获取指定的位置节点 6 DLNode<T> *p = GetElemP(i); 7 //插入新节点 8 Insert(p,e); 9 } 10 /** 11 *在指定的指针节点插入新的节点 12 */ 13 void Insert(DLNode<T> *p,const T &e){ 14 //创建一个新的节点 15 DLNode<T> *q = new DLNode<T>(e); 16 //设置新节点的向后的指针域(指向图中2节点) 17 q->next = p->next; 18 //设置新节点的向前的指针域(指向图中1节点) 19 q->prev = p; 20 21 //设置插入节点的后继节点的向前指针域(图中3节点的上一个节点指针域指向新节点2) 22 p->next->prev = q; 23 //设置插入节点的向后的指针域(指向新节点2) 24 p->next = q; 25 } 26 /** 27 *从头结点的位置插入新的节点 28 */ 29 void AddHead(const T &e){ 30 Insert(head,e); 31 } 32 /** 33 *从第一个节点的位置插入新的节点 34 */ 35 void Insert(const T &e){ 36 Insert(1,e); 37 }
3、删除节点
删除节点如下图,可以从图中看到,删除节点仅仅改变前驱和后继节点相关的指针域,这个就是为什么数据删除高手还可以找回来原因;
代码分析如下:
1 /** 2 *根据元素内容删除对应的节点 3 */ 4 void Delete(const T &e){ 5 //根据元素内容获取对应的节点 6 DLNode<T> *p = GetElemE(e); 7 //更多关于auto_ptr知识,请阅读memory里auto_ptr源码 8 std::auto_ptr<DLNode<T> > new_ptr(p); 9 //设置删除节点后继节点向前的指针域为删除节点向前指针域 10 p->next->prev = p->prev; 11 //设置删除节点前驱节点向后的指针域为删除节点向后指针域 12 p->prev->next = p->next; 13 }
4、遍历节点,在stl源码中有非常强大遍历神器(iterator),把泛型的知识运用到极致,感谢的朋友可以读读源码
1 /** 2 *遍历双向链表 3 */ 4 void Traverse(){ 5 //指向第一个节点信息 6 DLNode<T> *p = head->next; 7 8 //如果节点向后指针域等于头结点,表示链表已经遍历完成 9 while(p != head){ 10 std::cout<<"p is value = "<<p->data; 11 std::cout<<""<<std::endl; 12 p = p->next; 13 } 14 }
5、清空链表
1 /** 2 *清空整个链表节点 3 */ 4 void Clear(){ 5 //获取第一个节点信息 6 DLNode<T> *p = head->next; 7 8 //如果节点向后指针域等于头结点,表示链表已经被清空 9 while(p != head && p != 0){ 10 //更多关于auto_ptr知识,请阅读memory里auto_ptr源码 11 std::auto_ptr<DLNode<T> > new_ptr(p); 12 std::cout<<p; 13 std::cout<<std::endl; 14 //执行后继节点 15 p = p->next; 16 } 17 //恢复出厂原状,向前和向后的指针域都指向本身,构成双向链表 18 head->next = head->prev = head; 19 }
6、是否为空和计算链表长度
/** *判断链表是否为空 */ bool Empty(){ return head->next = head; } /** *计算链表的长度 */ int Length(){ int i = 0; DLNode<T> *p = head->next; //如果节点向后指针域等于头结点,链表遍历完成, while(p != head){ //执行后继节点 p = p->next; //累计链表节点长度 i++; } return i; }
7、测试代码
1 void test(){ 2 std::cout<<"-----------insert begin------------"<<std::endl; 3 for(int i = 1; i <= 5; ++i){ 4 Insert(i); 5 } 6 AddHead(6); 7 AddHead(7); 8 Traverse(); 9 std::cout<<"-----------insert end------------"<<std::endl; 10 11 std::cout<<"frist list length="<<Length()<<std::endl; 12 13 std::cout<<"-----------delete begin----------"<<std::endl; 14 Delete(2); 15 std::cout<<"-----------delete end----------"<<std::endl; 16 17 std::cout<<"second list length="<<Length()<<std::endl; 18 19 std::cout<<"-----------traversal of the elemetns begin----------"<<std::endl; 20 21 Traverse(); 22 Clear(); 23 Delete(1); 24 Clear(); 25 std::cout<<"third list length="<<Length()<<std::endl; 26 }
8、运行结果
9、完整代码
1 /* 2 * DLinkList.h 3 * 4 * Created on: 2012-10-13 5 * Author: hs 6 */ 7 8 #ifndef DLINKLIST_H_ 9 #define DLINKLIST_H_ 10 #include "core/node/DLNode.h" 11 12 template<class T> 13 class DLinkList { 14 private: 15 DLNode<T> *head;//头结点 16 /** 17 *根据位置获取相对应的节点信息 18 *size_t 是一个无符号的类型,确定调用该函数时入参一定是正数, 19 *这样的要求也提醒我们在写函数时,应该避免把这样函数暴露给客户 20 *不知道为什么linux很多api函数都有size_t类型的入参(通过调试,传入负数会导致严重的bug), 21 *有知道的朋友,请帮忙分享下 22 */ 23 DLNode<T>* GetElemP(size_t i) const{ 24 int j = 0; 25 DLNode<T> *p = head; 26 27 do{ 28 p = p->next; 29 j++; 30 }while(p != head && j < i); 31 32 return p; 33 } 34 35 /** 36 *根据元素获取对应的节点信息 37 */ 38 DLNode<T>* GetElemE(const T &e) const{ 39 DLNode<T> *p = head->next; 40 41 while(p != head && e != p->data){ 42 p = p->next; 43 } 44 45 return p; 46 } 47 /** 48 *在指定的位置插入节点 49 */ 50 void Insert(size_t i,const T &e){ 51 //获取指定的位置节点 52 DLNode<T> *p = GetElemP(i); 53 //插入新节点 54 Insert(p,e); 55 } 56 /** 57 *在指定的指针节点插入新的节点 58 */ 59 void Insert(DLNode<T> *p,const T &e){ 60 //创建一个新的节点 61 DLNode<T> *q = new DLNode<T>(e); 62 //设置新节点的向后的指针域(指向图中2节点) 63 q->next = p->next; 64 //设置新节点的向前的指针域(指向图中1节点) 65 q->prev = p; 66 67 //设置插入节点的后继节点的向前指针域(图中3节点的上一个节点指针域指向新节点2) 68 p->next->prev = q; 69 //设置插入节点的向后的指针域(指向新节点2) 70 p->next = q; 71 } 72 73 public: 74 /** 75 *构造函数,初始化头结点(向前和向后都指向本身,构成双向链表) 76 */ 77 DLinkList():head(new DLNode<T>(0)){ 78 head->next = head; 79 head->prev = head; 80 } 81 82 /** 83 *析构函数,释放所有新创建的节点 84 */ 85 ~DLinkList(){ 86 Clear(); 87 } 88 /** 89 *清空整个链表节点 90 */ 91 void Clear(){ 92 //获取第一个节点信息 93 DLNode<T> *p = head->next; 94 95 //如果节点向后指针域等于头结点,表示链表已经被清空 96 while(p != head && p != 0){ 97 //更多关于auto_ptr知识,请阅读memory里auto_ptr源码 98 std::auto_ptr<DLNode<T> > new_ptr(p); 99 std::cout<<p; 100 std::cout<<std::endl; 101 //执行后继节点 102 p = p->next; 103 } 104 //恢复出厂原状,向前和向后的指针域都指向本身,构成双向链表 105 head->next = head->prev = head; 106 } 107 /** 108 *判断链表是否为空 109 */ 110 bool Empty(){ 111 return head->next = head; 112 } 113 114 /** 115 *计算链表的长度 116 */ 117 int Length(){ 118 119 int i = 0; 120 DLNode<T> *p = head->next; 121 122 //如果节点向后指针域等于头结点,链表遍历完成, 123 while(p != head){ 124 //执行后继节点 125 p = p->next; 126 //累计链表节点长度 127 i++; 128 } 129 130 return i; 131 } 132 /** 133 *从头结点插入新的元素 134 */ 135 void AddHead(const T &e){ 136 Insert(head,e); 137 } 138 /** 139 *从第一个位置插入新的节点 140 */ 141 void Insert(const T &e){ 142 Insert(1,e); 143 } 144 145 /** 146 *根据元素内容删除对应的节点 147 */ 148 void Delete(const T &e){ 149 //根据元素内容获取对应的节点 150 DLNode<T> *p = GetElemE(e); 151 //更多关于auto_ptr知识,请阅读memory里auto_ptr源码 152 std::auto_ptr<DLNode<T> > new_ptr(p); 153 //设置删除节点后继节点向前的指针域为删除节点向前指针域 154 p->next->prev = p->prev; 155 //设置删除节点前驱节点向后的指针域为删除节点向后指针域 156 p->prev->next = p->next; 157 } 158 159 /** 160 *遍历双向链表 161 */ 162 void Traverse(){ 163 //指向第一个节点信息 164 DLNode<T> *p = head->next; 165 166 //如果节点向后指针域等于头结点,表示链表已经遍历完成 167 while(p != head){ 168 std::cout<<"p is value = "<<p->data; 169 std::cout<<""<<std::endl; 170 p = p->next; 171 } 172 } 173 174 void test(){ 175 std::cout<<"-----------insert begin------------"<<std::endl; 176 for(int i = 1; i <= 5; ++i){ 177 Insert(i); 178 } 179 AddHead(6); 180 AddHead(7); 181 Traverse(); 182 std::cout<<"-----------insert end------------"<<std::endl; 183 184 std::cout<<"frist list length="<<Length()<<std::endl; 185 186 std::cout<<"-----------delete begin----------"<<std::endl; 187 Delete(7); 188 std::cout<<"-----------delete end----------"<<std::endl; 189 190 std::cout<<"second list length="<<Length()<<std::endl; 191 192 std::cout<<"-----------traversal of the elemetns begin----------"<<std::endl; 193 194 Traverse(); 195 Clear(); 196 Delete(1); 197 Clear(); 198 std::cout<<"third list length="<<Length()<<std::endl; 199 } 200 }; 201 202 #endif /* DLINKLIST_H_ */
五:环境
1、运行环境:Ubuntu 10.04 LTS+VMware8.0.4+gcc4.4.3;
2、开发工具:Eclipse+make
六:题记
1、上面的代码难免有bug,如果你发现代码写的有问题,请你帮忙指出,让我们一起进步,让代码变的更漂亮和更健壮;
2、我自己能手动写上面代码,离不开郝斌、高一凡、侯捷、严蔚敏等老师的书籍和视频指导,在这里感谢他们;
3、鼓励自己能坚持把更多数据结构方面的知识写出来,让自己掌握更深刻,也顺便冒充下"小牛";
欢迎继续阅读“启迪思维:数据结构和算法”系列