线性表
线性表:零个或多个数据元素的有限序列
线性表的顺序存储结构
顺序存储结构指的是用一段地址连续的存储单元依次存储线性表的数据元素
描述顺序存储结构需要三个属性:
- 存储空间的起始位置
- 线性表的最大存储容量
- 线性表的当前长度
顺序表的插入和删除操作
对于顺序表来说,可以根据下标来获得某一个位置的数据元素,这个复杂度为O(1)
对于顺序表的插入操作,就要有以下的流程:
- 判断这个表是不是已经满了
- 如果线性表插入位置不合理,抛出异常
- 从最后一个元素向前遍历到要插入数据的位置,分别将他们向后移动一个位置
- 将要插入的元素插入进去
- 表长加一
对于顺序表的删除操作,流程如下:
- 如果删除位置不合理,抛出异常
- 取出要删除的元素
- 从删除位置开始遍历到最后一个元素位置,分别将他们都向前移动一个位置
- 表长减一
线性表顺序存储结构的优缺点
优点:
- 无需为表示表中元素之间的逻辑关系而增加额外的存储空间
- 可以快速地存储表中任一位置的元素
缺点:
- 插入和删除操作需要移动大量的元素
- 当线性表长度变化较大的时候,难以确定存储空间的容量
- 造成存储空间的碎片
线性表的链式存储结构
为了表示每个数据元素与其直接后继数据元素
之间的逻辑关系,对数据元素
来说,除了存储位置本身的信息之外,还需存储一个指示其后继的信息(即直接后继的存储位置)。我们把存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域。指针域中存储的信息叫做指针或者链;这两部分组成数据元素
的存储映像,称为结点
N个结点(的存储映像)链结成一个链表,即为线性表的链式存储结构,因为此链表中每个结点只包含一个指针域,所以叫单链表
一般来说我们把链表的第一个结点的存储位置叫做头指针
有时,我们为了更加方便的对链表进行操作,会在单链表的第一个结点前附设一个结点,称为头结点;头结点的数据域可以不存储任何信息,当然也可以存储如线性表的长度等附加信息,头结点的指针域存储指向第一个结点的指针;注意,若线性表为空,那么头结点的指针域也为空
头结点和头指针的异同:
- 头指针是指链表的第一个结点的指针;头结点是为了操作的统一和方便设立的,放在第一个元素的结点之前,其数据域一般无意义(也可以存放链表的长度)
- 头指针具有标识作用,所以常用头指针冠以链表的名字;有了头结点,对第一个元素的前插入结点和删除第一结点,其操作和其他结点的操作就统一了
- 无论链表是否为空,头指针均不能为空,头指针是链表的必要元素;头结点不一定是链表必要元素
单链表的读取
获取链表第i个数据的思路:
- 声明一个结点P指向链表的第一个结点,初始化j从1开始
- 当j < i的时候,就遍历链表,让P的指针向后移动,不断指向下一个结点,j累加1
- 若到链表末尾为P空,则说明第i个元素不存在
- 否则查找成功,返回结点P的值
实现代码算法如下:
单链表的插入和删除
单链表第i个数据插入结点的算法思路:
- 声明一个变量P指向链表第一个结点,初始化j从1开始
- 当j < i时,就遍历链表,让P的指针向后移动,不断指向下一结点,j累加1
- 若到链表末尾P为空,则说明第i个元素不存在
- 否则查找成功,在系统中生成一个空结点S
- 将数据元素e赋值给S-->data
- 单链表的插入标准语句是S-->next=P-->next, P-->next=S
示例代码:
单链表第i个数据删除结点的算法思路:
- 声明一结点P指向链表的第一个结点,初始化j从1开始
- 当j < i时,就遍历链表,让P的指针不断向后移动,不断指向下一个结点,j累加1
- 若到链表末尾P为空,则说明第i个元素不存在
- 否则查找成功,将欲删除的节点P->next赋值给q
- 单链表的标准删除语句P->next=q-next
- 将q中的数据赋给e,作为返回
- 释放q节点,返回成功
从整个算法来说,我们很容易推导出插入和删除的操作复杂度都是O(n),如果我们不知道第i个元素的指针的位置,单链表数据结构在插入和删除操作上,与线性表的顺序存储结构没有太大的优势,但是如果我们从第i个位置上插入10个元素,对于顺序存储结构意味着,每次插入都需要移动n-i个元素,每次都是O(n),而单链表,我们只需要在第一次时找到第i个位置的指针,此时为O(n),接下来只是简单的通过赋值移动指针而已,时间复杂度都是O(1),显然,对于插入或者删除数据越频繁的操作,单链表的效率优势就越明显;而且最重要的是单链表不会有溢出情况的发生
单链表的整表创建
对于顺序存储结构的创建,其实就是一个数组的初始化,即声明一个类型和大小额数组并赋值的过程;而单链表的结构和顺序表的不同,它是一个动态结构,对于每个链表来说,它所占用的空间和位置是不需要预先分配好的,可以根据系统的情况和实际的需求即时生成
所以创建单链表额过程就是一个动态生成链表的过程,即从‘空表’的初始状态起,依次建立各元素结点,并逐个插入链表
单链表整表创建的思路:
- 声明一个结点P和计数器变量i
- 初始化一空表L
- 让L的头结点的指针指向NULL。即建立一个带头结点的单链表
- 循环
- 生成一新结点赋值给P
- 随机生成数字赋值给P的数据域P-->data
- 将P插入到头结点与前一新结点之间
这一方法又叫头插法,就是将新结点放在第一的位置上
事实上我们也可以不这么干,我们也可以把每次新结点都插在终端结点的后面,这种算法称之为尾插法
单链表的整表删除
单链表整表删除的算法思路如下:
- 声明一个P和q
- 将第一个结点赋值给p
- 循环
- 将下一结点赋值给q
- 释放p
- 将q赋值给p
单链表结构和顺序存储结构优缺点
存储分配方式:
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表结构采用链式存储结构,用一组任意的存储单元存放线性表的元素
时间性能:
- 查找
- 顺序:O(1) 单链表:O(n)
- 插入和删除
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在知道某位置的指针之后,插入和删除的时间仅为O(1),在不知道的时候还是O(n)
空间性能:
- 顺序存储结构需要预先分配存储空间,分大了浪费,分小了容易溢出
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也补受限制
通过上面的对比,我们可以得到一些经验上的结论:
- 若线性表需要频繁查找,很少进行插入和删除操作,宜采用用顺序存储结构;若需要频繁插入和删除时,宜采用单链表结构
- 当线性表的元素个数变化较大或者根本不知道有多大的时候,最好用单链表结构,这样可以不需要考虑存储空间的大小问题
循环链表
对于单链表,由于每个结点只存储了向后的指针,到了尾标志就停止了向后链的操作,这样某一结点就无法找到它的前驱结点了,就像我们刚才说的,不能回到从前
将单链表中终端结点的指针端由空指针改为头结点,就使整个单链表形成了一个环,这种头尾相接的单链表称为单循环链表,简称循环链表
其实循环链表和单链表的主要差距就是在于循环的判断条件上,原来是判断p-->next是否为空,现在则是p-->next不等于头结点,则循环未结束
在单链表中,我们有了头结点时,我们可以用O(1)的时间的访问第一个结点,但是对于要访问最后一个结点,我们需要O(N)的时间复杂度,我们需要把整个链表扫描一遍
我们也可以通过设置尾指针的方式使得整个链表访问第一个或者最后一个结点的访问时间复杂度为O(1)
双向链表
我们在单链表中,有了next指针,这就使得我们要查找下一个结点的时间复杂度为O(1),可是如果我们要查找的上一结点的话,那最坏的时间复杂度就是O(n)了,因为我们每次都要从头开始遍历查找
为了克服单向性这一缺点,双向链表可以解决这个问题;双向链表是在单项链表的每个结点中,再设置一个指向其前驱结点的指针域,所以双向链表中的结点都有两个指针域,一个指向直接前驱,另一个指向直接后继
双向链表是单向链表中扩展出来的结构,所以它的很多操作都是和单链表相同的
但是需要注意的是,因为双向链表有两个指针域,所以在插入和删除的时候需要修改的就是两个指针域
插入操作:
- 假设需要在i处插入一个结点S
- 先将S的前驱指针域指向第i-1结点,然后将i-1结点的指针域指向结点S;接着讲S的后继指针指向原第i个结点,然后还要调整第i个结点的前驱指针的指向
删除操作和插入操作差不多