//TODO NullGirlfrindExceptionios
请忽略以上两行无聊的事实......算法
第一次学习线性表必定会立刻接触到一种叫作顺序表(顺序存储结构),通过上一篇的分析顺序表的优缺点是很显然的,它虽然可以很快的访问读取元素,可是在解决如插入和删除等操做的时候,却须要移动大量的元素,效率较低,那么是否有一种方法能够改善或者解决这个问题呢?数组
首先咱们须要考虑,为何顺序表中的插入删除操做会涉及到元素的移动呢?微信
好家伙,问题就是围绕着顺序表的最大的特色出现的——顺序存储,相邻放置元素,也就是说每一个元素都是根据编号一个一个挨着的,这就致使了 插入或删除后,为了仍然呈顺序线性存储,被操做元素后面的元素的位置均须要发生必定的变化,你应该能想象获得,在拥挤的队伍中忽然从中插入一个学生的场景,后面浩浩荡荡的人群,口吐芬芳的向后挪了一个空位,若是人群过大,从新排好队也须要必定的时间函数
好嘛,人与人之间别这么挤在一块儿,每一个人与人之间都流出一点空隙来,留必定的位置出来,好了,这好像是个办法,可是负责一个一个与学生交流填表的老师可就不干了,这意味着我(找人)遍历的时候,须要多跑好多路,浪费好多时间,先不说这个,体院馆又不行了,大家这么个摆法,我这小馆可放不下,这也就意味着空间复杂度增长了不少学习
咱们刚才所围绕的都是在 "排队" 的基本前提下的,但咱们能想到的方法并非很理想,那么咱们索性就不排队了,是否是能有更好的解决方式呢?ui
一个有效的方法:this
让同窗们(元素)本身找位置随便站,不过你要知道相对于本身下一位同窗的位置,这样既解决了空间上的问题,又能经过这种两两联系的方式访问(遍历)到整个队伍(数组),最重要的是,插入和离开同窗,因为同窗(元素)之间不存在了那种排队,相邻的特色,因此也不会说影响到过多的同窗(元素)只须要和你插入位置的先后两位同窗沟通好就好了,反正别人也不知道大家之间发生了什么事spa
好了思路是有了,咱们来看一种最多见的链表——单链表设计
这种链表为何被称做单链表呢?这是由于它只含有一个地址域,这是什么意思呢?
咱们在链表中摈弃了顺序表中那种一板一眼的排队方式,可是咱们必须让两个应该相邻的元素之间有必定的相互关系,因此咱们选择让每个元素能够联系对应的下一个元素
而这个时候咱们就须要给每一个元素安排一个额外的位置,来存储它的后继元素的存储地址,这个存储元素信息的域叫作指针域或地址域,指针域中储存的信息也叫做指针或者链,
咱们用一张图 看一下他的结构
结构中名词解释
咱们来解释一下:
链表若是为空的状况下,若是单链表没有头结点,那么头指针就会指向NULL,若是加上头结点,不管单链表是否为空,头指针都会指向头结点,这样使得空链表与非空链表处理一致
使首元结点前插入或删除元素的时候,与后面操做相同,不须要产生额外的判断分支,使得算法更加简单
(以插入为例讲解)在带头结点的状况下,在首元结点前插入或者删除元素仍与在其余位置的操做相同,只须要将前一个元素(在这里是头结点)的指针域指向插入元素,同时将插入元素的指针域指向原来的第二的元素
而无头结点的状况因为,首元结点前没有元素,只能经过修改head的先后关系,因此致使了 与在别的位置插入或删除元素的操做不一样,在实现这两个功能的时候就须要额外的写一个判断语句来判断插入的位置是否是首元结点以前的位置,增长了分支,代码不够简洁
总结:头结点的存在使得空链表与非空链表处理一致,也方便对链表首元结点前结点的插入或删除操做
咱们在给出单链表的定义以前咱们仍是须要先引入咱们线性表的抽象数据类型定义
#ifndef _LIST_H_ #define _LIST_H_ #include<iostream> using namespace std; class outOfRange{}; class badSize{}; template<class T> class List { public: // 清空线性表 virtual void clear()=0; // 判空,表空返回true,非空返回false virtual bool empty()const=0; // 求线性表的长度 virtual int size()const=0; // 在线性表中,位序为i[0..n]的位置插入元素value virtual void insert(int i,const T &value)=0; // 在线性表中,位序为i[0..n-1]的位置删除元素 virtual void remove(int i)=0; // 在线性表中,查找值为value的元素第一次出现的位序 virtual int search(const T&value)const=0; // 在线性表中,查找位序为i的元素并返回其值 virtual T visit(int i)const=0; // 遍历线性表 virtual void traverse()const=0; // 逆置线性表 virtual void inverse()=0; virtual ~List(){}; }; /*自定义异常处理类*/ class outOfRange :public exception { //用于检查范围的有效性 public: const char* what() const throw() { return "ERROR! OUT OF RANGE.\n"; } }; class badSize :public exception { //用于检查长度的有效性 public: const char* what() const throw() { return "ERROR! BAD SIZE.\n"; } }; #endif
#ifndef _SEQLIST_H_ #define _SEQLIST_H_ #include "List.h" #include<iostream> using namespace std; template<class elemType> //elemType为单链表存储元素类型 class linkList:public List<elemType> { private: //节点类型定义 struct Node { //节点的数据域 elemType data; //节点的指针域 Node *next; //两个构造函数 Node(const elemType value, Node *p = NULL) { data = value; next = p; } Node(Node *p = NULL) { next = p; } }; //单链表的头指针 Node *head; //单链表的尾指针 Node *tail; //单链表的当前长度 int curLength; //返回指向位序为i的节点的指针 Node *getPostion(int i)const; public: linkList(); ~linkList(); //清空单链表,使其成为空表 void clear(); //带头结点的单链表,判空 bool empty()const {return head -> next == NULL;} //返回单链表的当前实际长度 int size()const {return curLength;} //在位序i处插入值为value的节点表长增1 void insert(int i, const elemType &value); //删除位序为i处的节点,表长减1 int search(const elemType&value)const; //查找值为value的节点的前驱的位序 int prior(const elemType&value)const; //访问位序为i的节点的值,0定位到首元结点 elemType visit(int i)const; //遍历单链表 void traverse()const; //头插法建立单链表 void headCreate(); //尾插法建立单链表 void tailCreate(); //逆置单链表 void inverse(); };
单链表的初始化就是建立一个带头节点的空链表,咱们不须要设置其指针域,为空便可
template<class elemType> linkList<elemType>::linkList() { head = tail = new Node(); curLength=0; }
注意:new 操做符表明申请堆内存空间,上述代码中应该判断是否申请成功,为简单,默认为申请成功,实际上若是系统没有足够的内存可供使用,那么在申请内存的时候会报出一个 bad_alloc exception 异常
当单链表对象脱离其做用域时,系统自动执行析构函数来释放单链表空间,其实也就是清空单链表内容,同时释放头结点
template<class elemType> linkList<elemType>::~linkList() { clear(); delete head; }
清空单链表的主要思想就是从头结点开始逐步将后面节点释放掉,可是咱们又不想轻易的修改头指针head的指向,因此咱们引入一个工做指针,从头结点一直移动到表尾,逐步释放节点
template<class elemType> void linkList<elemType>::clear() { Node *p, *tmp; p - head -> next; while(p != NULL) { tmp = p; p = p -> next(); delete tmp; } head -> next = NULL; tail = head; curLength = 0; }
因为咱们的代码中已经定义过一个叫作 curLength 的变量用来记录咱们的表长
因此咱们能够直接返回,咱们在定义中已经实现了,也就是这句
//返回单链表的当前实际长度 int size()const {return curLength;}
可是若是咱们没有这样一个变量,咱们想要实现这样的功能又是什么样的方法呢?
template<class elemType> int linkList<elemType>::size()const { Node *p = head -> next; int count; while(p) {count++; p = p -> next;} return count; }
咱们须要从头至尾访问单链表中的每个节点,而且输出其中数据域的信息
template<class elemType> void linkList<elemType>::traverse()const { Node *p = head -> next; cout << "traverse:"; while (p != NULL) { cout << p -> date << " "; p = p -> next; } }
设置一个移动工做指针,和一个计数器 count,初始时p指向头结点,每当指针p移向下一个结点的时候,计数器count + 1 ,直到 p指向位序为 i的节点为止。返回 p
template<class elemType> typename linkList<elemType>::Node *linkList<elemType>::getPostion(int i)const { if(i < -1 || i > curLength - 1) return NULL; Node *p = head; int count = 0; while(count <= i) { p = p -> next; count++; } return p; }
设置一个移动工做指针,和一个计数器 count,从单链表的第一个节点开始,开始于给定的值进行比对,若是相等则查找成功,返回节点的位序,不然继续查询知道单链表结束,查询失败返回 -1
template<class elemType> int linkList<elemType>::search(const elemType&value)const { Node *p = head -> next; int count = 0; while (p != NULL && p -> data != value) { p = p -> next; count++; } if (p == NULL) { return -1; }else { return count; } }
在位序为 i 出插入值为value 的新节点q,咱们须要作的就是找到位序为i - 1 的节点p,让q指针域指向原来p的后继,而后修改p的后继为q便可,说白了也就是修改插入元素位置先后的元素指向关系就能够了
template<class elemType> void linkList<elemType>::insert(int i,const elemType &value) { Node *p, *q; if(i < 0 || i > curLength) throw outOfRange(); p = getPostion(i - 1); q = new Node(value,p -> next); p -> next = q; if (p == tail) tail = q; curLength++; }
能看懂添加节点的方法,理解删除节点也是手到擒来
template<class elemType> void linkList<elemType>::remove(int i) { //p是待删节点,pre是其前驱 Node *p, *pre; if(i < 0 || i > curLength) throw outOfRange(); pre = getPostion(i - 1); p = pre -> next; if (p == tail) { tail = pre; pre -> next = NULL; delete p; } else { pre -> next = p -> next; delete p; } }
回顾咱们前面认识的顺序表,它其实能够理解为一个数组,咱们声明一个类型,同时给定值,初始化其大小,可是单链表就不同了,它是一种动态组织,它不须要像顺序表同样元素集中,它能够随着实际的状况来动态生成节点,因此也不须要预先分配空间大小和位置
头插法的意思就是说,每次新增节点所有插在头结点以后,首元结点以前,你能够这样理解,我先来排队,可是后面来了人,他就会排到个人前面去,咱们来借助图看一下
咱们一次插入元素 123 但实际上输出的是按照321的顺序存储的,也就是说和咱们的逻辑顺序是相反的
咱们来看一看怎么实现它
template<class elemType> void linkList<elemType>::headCreate() { Node *p; elemType value, flag; cout << "inputelements, ended with:"; cin >> flag; while(cin >> value, value != flag) { //p -> data == value, p -> next = head ->next p = new Node(value, head -> next); head -> next = p; //原链表为空,新节点p成为为节点 if (head == tail) tail = p; curLength++; } }
咱们知道单链表中元素顺序与读入的顺序是相反的,咱们能够经过逆置单链表的算法,帮助咱们从新恢复咱们的惯有思惟顺序
template<class elemType> void linkList<elemType>::inverse() { Node *p, *tmp; //p为工做指针,指向首元结点 p = head -> next; //头结点的指针域置空,构成空链表 head -> next = NULL; //逆置后首元结点将成为尾节点 if (p) tail = p; while (p) { //暂存p的后继 tmp = p -> next; p -> next = head -> next; //节点p插在头结点的后面 head -> next = p; //继续处理下一个节点 p = tmp; } }
看完了头插法,可是感受这样的顺序与咱们一向的思惟老是有一点别扭,而尾插法则是一种,逻辑顺序与咱们一致的建立方法
仍是看一下图
template<class elemType> void linkList<elemType>::tailCreate() { Node *p; elemType value, flag; cout << "inputelements, ended with:"; cin >> flag; while(cin >> value, value != flag) { p = new Node(value,NULL); tail -> next = p; tail = p; curLength++; } }
要求:假设咱们给出两个仍然是递增的单链表la和lb,咱们将其合并为lc 仍保证递增,利用原表空间,可是咱们仍在下面将表C称做新表
由于咱们的要求是递增的,因此使用尾插法是很是合适的,咱们设计三个工做指针,分别指向两个表的首元结点,而后将第三个指针指向新表的头结点,比较前两个指针指向的值,小的就放到新表的表尾,而后后移动两表中较小的那一个的指针,以此类推,直到其中一个表尾空,将剩余的节点所有连接到新表的末尾
template<class elemType> typename linkList<elemType> *linkList<elemType> ::Union(linkList<elemType> *lb) { Node *pa, *pb, *pc; linkList<elemType> *lc = this; pa = head -> next; head -> next = NULL; pb = (lb -> head) -> next; (lb -> head) -> next = NULL; pc = lc -> head; while(pa && pb) { if(pa -> data <= pb -> data) { pc-> next = pa; pc = pa; pa = pa -> next; } else { pc -> next = pb; pc = pb; pb = pb -> next; } } if(pa) { pc -> next = pa; lc -> tail = tail; } else { pc -> next = pb; lc -> tail = lb -> tail; } lc -> cuirLength = curLength + lb -> curLength; delete lb; return lc; }
单链表,采起了链式存储结构,用一组任意的存储单元存放线性表的元素,尤为对于须要频繁的插入和删除数据的时候更加适用,若是须要进行频繁的查找仍是推荐使用顺序表,例如对于一个学生成绩管理系统的制做,学生更多的时候是查看本身的成绩,而录入的老师,也只有在考试后录入一次,因此应该使用顺序表,而例如考勤打卡系统,更多的是打卡信息的记录,因此仍是选择使用链表,固然例子可能不是很恰当,同时正常的开发中还会有更多复杂的问题须要考虑,举例子只为了利于理解
若是文章中有什么不足,或者错误的地方,欢迎你们留言分享想法,感谢朋友们的支持!
若是能帮到你的话,那就来关注我吧!若是您更喜欢微信文章的阅读方式,能够关注个人公众号
在这里的咱们素不相识,却都在为了本身的梦而努力 ❤
一个坚持推送原创开发技术文章的公众号:理想二旬不止