数据结构与算法1- 单链表 循环链表和跳跃链表(SkipList)
来源:互联网 发布:个人博客 php 源码 编辑:程序博客网 时间:2024/05/17 00:16
数据结构与算法1- 单链表 循环链表和跳跃链表(SkipList)
写在前面
链表是数据结构中基础数据结构,这里作为实践学习,实现和分析了单链表、循环链表和跳跃链表。
1.为什么引入链表?
传统数组实现的线性表,能够支持随机存取,即以O(1)的复杂度读写元素。传统数组方式实现存在两个弊端。
其一,在删除和插入元素时,除了要定位删除和插入的位置外,还要移动数组中其他元素,导致了很大的开销。
例如在有序表[3 4 5 7 9 13]中,删除3,则3后面的元素都将向前移动一次,共移动5次;如果要添加元素2的话,则3开始的元素都要往后端移动一次,共移动6次。假设线性表长度为n,则:
对于删除情况,假设删除n个节点的概率相同的,令X表示删除节点时元素移动次数这个随机变量,则X的平均值为:
对于插入情况,n个节点有n+1个插入位置,假设n+1个位置插入的概率相同,令Y表示插入时元素移动次数的随机变量,则Y的平均值为:
其二, 顺序数组在元素不断增长超过原先分配的连续内存区大小时,需要动态扩容时,复制元素需较大的额外开销。
基于此,引进链表来作为线性表的一种实现方式。链表在插入和删除方面,只需要找到删除或者插入位置即可,不需要移动元素,比数组开销小;当然链表不支持随机访问,这一点也是一个显著特点。
2.常见链表概念及其实现
典型的存贮无序整型值的,单链表、循环单链表、循环双链表如下图所示:
循环单链表比之于单链表区别在于,循环单链表表尾指针指向了头部,形成一个环,这样能快速找到头结点和尾结点,因此无论头插法还是尾插法,时间复杂度均为O(1);
双链表比之于单链表区别在于,双链表每个节点多了一个前向prev指针,因此一个结点能快速找到他的前驱和后继结点,对于尾部删除来讲,单链表时间复杂度为O(n),而双链表则为O(1)。
这三类表完整的操作可能因不同实现二一,c++标准库里面的链表实现方法众多,这里做出简化。
定义这三个类的的公共操作如下:
template <class T>class List{public: virtual ~List(){} //add methodsvirtual void addFromHead(const T& data)=0;virtual void addFromTail(const T& data)=0;//delete methodsvirtual void removeFromHead()=0;virtual void removeFromTail()=0;virtual void remove(const T& data)=0;//check methodsvirtual bool containsOf(const T& data)=0;virtual bool isEmpty()=0;//get methodsvirtual int getSize()=0;//for testing,print list contentvirtual void print()=0;};实现单链表结点定义如下:
template <class T>class SNode{public:SNode(){ next = 0; } SNode(T v,SNode* n=0):data(v),next(n){}public:T data;SNode* next;};
实现双链表的结点定义如下:
template<class T>class DNode{public:DNode(){ prev = next = 0; }DNode(T v,DNode* p,DNode* n):data(v),prev(p),next(n){}public:T data; DNode *prev,*next;};
链表实现的关键点在于: 动态的维护保存链表头结点和尾结点的指针,以及在插入删除时维护相关结点的指针。
例如有一个head和tail的单链表SLL的删除结点方法如下:
template <class T>void SLL<T>::remove(const T& data){ if(head == 0) return; if(head->data == data) { removeFromHead();//delete head node }else { SNode<T>* prior = head; while(prior->next != 0 && prior->next->data != data) prior = prior->next; if(prior->next != 0) { SNode<T>* pRmove = prior->next; prior->next = pRmove->next; if(pRmove->next == 0) tail = prior;//if remove last node,reset the tail delete pRmove; //delete middle node } }}例如有一个tail指针的循环单链表CSLL删除结点方法如下:
template<class T>void CSLL<T>::remove(const T& data){ if(tail != 0){ SNode<T> *prior=tail,*pRemove = tail->next; while(pRemove != tail && pRemove->data != data) { prior = prior->next; pRemove = pRemove->next; } if(pRemove != tail ) { prior->next = pRemove->next; delete pRemove; }else if(tail->data == data) { if(tail->next == tail) //only one node { delete tail; tail = 0; }else { prior->next = tail->next; delete tail; tail = prior; } }}}例如有一个tail指针的循环双链表CDLL删除结点方法如下:
template<class T>void CDLL<T>::remove(const T& data){ if(tail != 0){ DNode<T>* pTmp = tail->next; while(pTmp != tail && pTmp->data != data) pTmp = pTmp->next; if(pTmp != tail) //match 1..tail-1 node { pTmp->prev->next = pTmp->next; pTmp->next->prev = pTmp->prev; delete pTmp; }else if(tail->data == data) //match tail node { if(tail == tail->next) //only one node { delete tail; tail = 0; }else { tail->prev->next = tail->next; tail->next->prev = tail->prev; DNode<T>* pTmp = tail->prev; delete tail; tail = pTmp; } }}}
这里利用C++的模板和多态机制,就可以实现由List基类指向三个链表类的实例,测试例程参见附录1。
3.跳表(SkipList)实现
这里分析并给出跳跃链表的一个实现。对于跳表的完整情况,可以参考课件:http://www.kernelchina.org/algorithm/SL.ppt,这里不再对每种操作的细节作出完整概述,而主要强调实现过程中几个注意点。
跳表如下图所示(来自课件:http://www.kernelchina.org/algorithm/SL.ppt):
1)为什么需要跳表呢?
如上图所示的跳表,当我们查找37时,top指针指向Level3,首先在第三层查找。与21比较后与37比较,查找成功,立即返回true即可。从这里即可初步看出,跳表查找的优势,如果是从具有相同元素的单链表中查找37则比较次数为5次。实际上,在最好的情况下,跳跃链表查找时间为O(lgn)。最坏情况下,所有节点都在同一级上,跳跃链表变成了正规的单链表,且查找时间为O(n)。但是跳跃链表中这种最坏情况不太可能出现,因为跳跃链表是基于随机算法的。在随机的跳跃链表中,查找时间最坏情况下也为O(lgn)。因此,跳跃链表的查找的时间复杂度即为O(lgn),比前面几种类型的链表都要优秀,因此引入跳跃链表。
2)跳跃链表的结构
如上图所示,跳跃链表的特点如下(来自:跳表(Skip List)的介绍以及查找插入删除等操作):
- 一个跳表应该有几个层(level)组成;
- 跳表的第一层包含所有的元素;
- 每一层都是一个有序的链表;
- 如果元素x出现在第i层,则所有比i小的层都包含x;
- 第i层的元素通过一个down指针指向下一层拥有相同值的元素;
- 在每一层中,-1和1两个元素都出现(分别表示INT_MIN和INT_MAX);
- Top指针指向最高层的第一个元素。
实际上在实际实现时,与上图有区别,主要在于:
1)作为泛型链表来讲,每层里面不应该设置最大和最小值来作为界限,例如上图所指的-1和1,或者是INT_MAX和INT_MIN都有可能成为实际中的值;
2)上图中,每一层都各自成为一个链表,实际上在内存结构上会被所有的层共享结点这种方式取代。
实现中的跳跃链表的内存结构如下图所示:
每个结点定义为:
#define MAX_LEVEL 16template<class T>class SkipNode{public: SkipNode(){for(int i = 0;i < MAX_LEVEL ;i++)next[i] = 0; } SkipNode(T k):key(k){ for(int i = 0;i < MAX_LEVEL ;i++)next[i] = 0;}public:T key; SkipNode* next[MAX_LEVEL];};
当然,每个结点里面使用MAX_LEVEL个指针,其实也需要改进,可以使用指向保存了多个指针的指针数组来解决。不过这样实现的代码,比使用数组困难些,在这里就不做讨论了。
3)跳跃链表的操作
跳跃链表要支持查找、删除和插入三种基本操作,其实现方式与普通链表存在较大的差异。
跳表定义为:
template<class T>class SkipList{public: SkipList(); ~SkipList(); T* find(const T& key); bool insert(const T& key); bool remove(const T& key); bool isEmpty(); void print();private:int getRandomLevel();private: int maxLevel;typedef SkipNode<T>* SkipNodePtr; SkipNodePtr head;};
实现关键点在于: 维护保存跳表各层结点信息的top或者head指针,以及在插入和删除时动态维护各个节点的next指针。
课件:http://www.kernelchina.org/algorithm/SL.ppt对此作了比较好的介绍,可以参考里面的详细介绍。
基于上面的跳跃链表的内存结构给出的实现时,着重强调两点:
1)在如下图所示的有序表中查找时,有三种情况时停止:
第一种 p->next != 0 && p->next->key == key,这种情况,即找到了所查找元素的前驱;
第二种 p->next != 0 && p->next->key > key,这种情况,即找到了比所查找元素大的元素中最小的那个元素的前驱,也即所查找元素要插入位置的前驱;
第三种 p->next == 0,这种情况,表为空或者已经找到了表尾,但是所有元素都小于所要查找的元素,此时等价于找到了所查找元素要插入的位置的前驱。
这几种情况是很重要的,需要明确。
2)插入和删除结点是,都只是针对一个结点操作,在所有层上维护指针。不要以为同一个结点有多个备份链接在表中的不同层上。
另外对于同一个结点而言,要指向同一层的next结点,需要使用层号作为next指针数组的索引,例如p->next[level-1],代表的就是p结点在第level层的下一个结点;同时在向下一层展开搜索时,只需改变层的索引即可,例如: level--;p->next[level-1]即代表从当前结点p的下一层的开始比较。
跳表的查找、删除和插入三种基本操作的实现如下:
template<class T>SkipList<T>::SkipList():maxLevel(1){ head = new SkipNode<T>();}template<class T>T* SkipList<T>::find(const T& key){ int curLevel = maxLevel; SkipNodePtr pCur = head; while(curLevel > 0) {while(pCur->next[curLevel-1] != 0 && pCur->next[curLevel-1]->key < key)pCur = pCur->next[curLevel-1];if(pCur->next[curLevel-1] != 0 && pCur->next[curLevel-1]->key == key)return &pCur->next[curLevel-1]->key;//if match ,return immediately curLevel--;//search down layer } return 0;}template<class T>bool SkipList<T>::insert(const T& key){ SkipNodePtr priorInsertPos[MAX_LEVEL];int curLevel = maxLevel;SkipNodePtr pCur = head;while(curLevel > 0){while(pCur->next[curLevel-1] != 0 && pCur->next[curLevel-1]->key < key) pCur = pCur->next[curLevel-1];if(pCur->next[curLevel-1] != 0&& pCur->next[curLevel-1]->key == key)return false;//if already exsit ,return falsepriorInsertPos[curLevel-1] = pCur;//store insert positioncurLevel--;//search down layer } int levelCnt = getRandomLevel();//get random level count SkipNodePtr pInsert = new SkipNode<T>(key);//insert new node from level 1 to levelCntfor(int i = 1;i <= levelCnt;i++){if(i > maxLevel){head->next[i-1] = pInsert;}else{ pInsert->next[i-1] = priorInsertPos[i-1]->next[i-1];priorInsertPos[i-1]->next[i-1] = pInsert;}}if(levelCnt > maxLevel)maxLevel = levelCnt; //update max levelreturn true;}template<class T>bool SkipList<T>::remove(const T& key){ SkipNodePtr priorRemovePos[MAX_LEVEL];SkipNodePtr pRemove = 0;int curLevel = maxLevel;int startFindLevel = -1; //-1 represent not find//search all levels,record position prior to removeSkipNodePtr pCur = head;while(curLevel > 0){ while(pCur->next[curLevel-1] != 0 && pCur->next[curLevel-1]->key < key) pCur = pCur->next[curLevel-1];if(pCur->next[curLevel-1] != 0 && pCur->next[curLevel-1]->key == key){ if(startFindLevel == -1){startFindLevel = curLevel;pRemove = pCur->next[curLevel-1];}priorRemovePos[curLevel-1] = pCur;//store pos prior to remove}curLevel--;//search down layer } if(startFindLevel == -1) return false;//update the reference pointer for(int i = startFindLevel;i > 0;i--){ priorRemovePos[i-1]->next[i-1] = pRemove->next[i-1]; if(pRemove->next[i-1] == 0) maxLevel--; // reduce one level,update maxLevel}//delete the node only onecedelete pRemove;return true;}
跳跃链表的测试结果参见附录2。
参考资料:
[1] 数据结构与算法 c++版 第三版 Adam Drozdek编著 清华大学出版社
[2] 数据结构 严蔚敏 吴伟明 清华大学出版社
[3] 跳表(Skip List)的介绍以及查找插入删除等操作[4]课件:http://www.kernelchina.org/algorithm/SL.ppt
[5] Skip List(跳跃表)原理详解与实现
附录1:链表测试结果
单链表测试:init content: SLL [3 7 4 5 9 ]remove 3: SLL [7 4 5 9 ]remove 9: SLL [7 4 5 ]contain -1?: nocontain 4?: yesremove head: SLL [4 5 ]add tail 12: SLL [4 5 12 ]list size: 3remove 5: SLL [4 12 ]remove tail: SLL [4 ]delete list: Free SLL space循环单链表测试:init content: CSLL [3 7 4 5 9]remove 3: CSLL [7 4 5 9]remove 9: CSLL [7 4 5]contain -1?: nocontain 4?: yesremove head: CSLL [4 5]add tail 12: CSLL [4 5 12]list size: 3remove 5: CSLL [4 12]remove tail: CSLL [4]delete list: Free CSLL space循环双链表测试:init content: CDLL [3 7 4 5 9]inverse print: CDLL [9 5 4 7 3]remove 3: CDLL [7 4 5 9]remove 9: CDLL [7 4 5]contain -1?: nocontain 4?: yesremove head: CDLL [4 5]add tail 12: CDLL [4 5 12]list size: 3remove 5: CDLL [4 12]remove tail: CDLL [4]delete list: Free CDLL space
附录2: 跳跃链表测试结果
跳跃链表测试一:SkipList []find 12 : noinsert 7:-----------------------------------SkipList [3 : 7||2 : 7||1 : 7]-----------------------------------remove 7:SkipList []insert 12:insert 45:insert 32:-----------------------------------SkipList [2 : 45||1 : 12 32 45]-----------------------------------remove 12:-----------------------------------SkipList [2 : 45||1 : 32 45]-----------------------------------insert 27:insert 88:find 88 : yes-----------------------------------SkipList [2 : 45||1 : 27 32 45 88]-----------------------------------remove 45:-----------------------------------SkipList [1 : 27 32 88]-----------------------------------modify element 88 to 119:-----------------------------------SkipList [1 : 27 32 119]-----------------------------------Free SkipList space请按任意键继续. . .测试结果2:insert a:insert f:insert b:insert k:insert e:insert d:-----------------------------------SkipList [3 : a||2 : a b||1 : a b d e f k]-----------------------------------remove f:-----------------------------------SkipList [3 : a||2 : a b||1 : a b d e k]-----------------------------------Free SkipList space请按任意键继续. . .
- 数据结构与算法1- 单链表 循环链表和跳跃链表(SkipList)
- 数据结构和算法系列 - 跳跃表 SkipList
- 跳跃链表 skipList
- Redis-数据结构-跳跃表-skiplist
- 跳跃表 SkipList【数据结构】原理及实现
- 跳跃表skiplist
- 跳跃表skiplist简析
- SkipList跳跃表详解
- 跳跃表skiplist
- SkipList 跳跃表
- SkipList跳跃表
- SkipList跳跃表
- skiplist 跳表详解及其编程实现--跳跃链表
- Redis内部数据结构详解之跳跃表(skiplist)
- Redis内部数据结构详解之跳跃表(skiplist)
- Redis之跳跃表SkipList
- 【数据结构】跳跃列表 SkipList
- Redis源码剖析和注释(四)--- 跳跃表(skiplist)
- 数字媒体技术揭秘
- Android 连接MongoDB与基本操作
- sgu 194上下界网络流
- mybatis----配置
- Swap - HDU 2819 二分图匹配
- 数据结构与算法1- 单链表 循环链表和跳跃链表(SkipList)
- [问题]CocoaPods:XX(from `./`)` required by `Podfile`
- Maven常见异常及解决方法 .
- PL/SQL Developer中Dynamic Performance Tables Not Accessible的解决
- 局部变量的类型
- Android中对闹钟Alarm的事件处理
- 无边窗体在任务栏上的系统菜单
- 【BMT】MTK电池充电问题
- 【DButils学习之】总结