一、线性表的定义:
线性表是一种可以存放数据元素的一条线或表,而且程线性存储。当我们要存放的数据元素是整形之类的单个元素的时候,那么线性表就像一条线一样,这条线上串着整形的不同元素。
而当我们要存放的元素是struct结构体这种的复杂元素的时候,每个元素都包含各种数据项(数据元素由数据项组成)。比如一个struct结构体存放着学生的姓名,性别,学号等元素。那么这时候存放struct结构体的线性表就像一个表一样,这也是我认为线性表被称为 “表” 的原因第一行存放一个结构体,第一行的内容就是结构体中的内容。
这就是线性表(上述是我用自己的语言来描述我心中的线性表)。
既然线性表是一种存放数据的线或表,那么就会涉及到创建,销毁,增加元素,删除元素,改变元素,查找元素等操作。那么在接下来的顺序表和链表中,我就会详细讲到顺序表和链表在完成这些操作的时候所涉及到的问题。
二、顺序表:
顺序表,顾名思义,表中的元素是按顺序存储的,这与我们所认知的数组的性质很相似,所以这里我们就用数组来表示一个顺序表,每个表都对应一个下标,第一个元素下标为0,第二个为1后面的元素依次增长。
而创建一个顺序表有两种方法。
第一种方法就是直接创建一个数组(静态存储)这种顺序表的创建方法就是直接在编写程序的时候直接创建一个数组。而如果我们要销毁这个顺序表的话,由于静态存储是储存在内存中的栈区,操作系统会在这个顺序表的生命周期结束的时候自动回收。
第二种方法就是引用malloc函数和free函数(动态存储,可在运行时自由分配此顺序表的空间大小),(这两个函数需要引用<stdlib.h>头文件)。这里我们用整形来举例:
int* arr = (int* )malloc(sizeof(n)) (int* )表示类型转换,(sizeof(n)) 表示一个元素的大小。
如果我们要销毁这个顺序表的话,由于动态储存是将顺序表储存到堆区,在用完该表后,操作系统不会自动帮我们回收这片空间,那么此时我们就需要一个函数free()。
我们了解了顺序表的存储方式之后,对于增删改查这四个操作,顺序表比较擅长哪些操作呢?我们知道,顺序表每个元素都有下标,那么,如果我们要查找一个元素在这个顺序表中的位置的话,就轻而易举了利用arr+i就可以做到。
但是,如果我们要在顺序表中增加一个元素,由于每个元素都有对应的下标,那么最好的情况就是在表位增加一个元素,只需要直接增加该元素就可以完成该操作,但是如果是在中间的任意位置,甚至是在表头增加一个元素。那么我们就要将插入位置后面的元素全部向后移位。再把这个元素增加到这个位置。那么时间复杂度就为O(n),但是这种数据元素的移动有一个弊端,如果一个数据非常大甚至一个数据就占了1MB的内存,移动1个数据元素就需要很久的时间,移动数量级为n个元素所需要的时间就会不可估计,删除一个元素同理。
引:链表分为单链表和双链表,有头结点的链表和没有头结点的链表。理论上这两者都可行,但是在我们实际写代码的过程中,有头结点的链表更容易来进行操作
三、单链表:
1.单链表定义
顾名思义表中的每个元素都由一个 “链子” 连起来,这个 “链子”就是指针,单链表中的一个元素称为一个结点,而结点是由数据域(data)和指针域组成,而指针又指向了下一个元素,最后一个结点的指针指向空指针NULL。
2.单链表的建立
在我们创建一个链表的时候还需要引入一个头指针L,这样才能保证链表的第一个结点也有指针来指向。在有头结点的情况下,如果我们要创建一个单链表。头指针L指向头结点,头结点的指针指向NULL,那么我们就可以在头结点->NULL的范围内插入元素。
比如,在单链表内要插入一个元素e,那么先创建一个结点s(数据域为e)。然后将s结点的指针指向前驱结点的指针域中指针指向的数据元素,然后再将前驱节点的指针指向结点s的数据域。
这里要注意:插入操作的顺序要严格按照上述顺序,不然会出BUG。这里可以自行思考为什么会出现BUG?
但是如果是在没有头结点的情况下来插入一个结点s,那么我们如果要在一个单链表的第一个位置插入。由于没有头结点,我们就不能把头结点当成第0个结点,然后仿照在单链表中间插入的操作进行首元素插入。此时我们就需要特殊处理首元素的插入,这也就是为什么,在写代码的过程中有头结点比没有头结点方便一些。
3.单链表的插入
我们在了解了链表的插入逻辑之后,有三种插入方式需要我们了解。
(1)指定结点后插操作:由于单链表的储存方式是结点的指针指向后一个结点的数据域,那么如果给定一个结点p,那么通过结点p的指针我们就可以找到p的后继节点q,那么在p结点和q结点之间,我们就可以进行前面讲的插入操作。由于给定了一个结点,所以不需要我们从头指针L向后一次遍历一遍单链表来找到p结点,故此操作的时间复杂度为O(1)。
(2)在第i个位置插入操作:这种操作与指定结点的后插操作类似,不过由于没有给定位置 i 所对应的结点,那么我们就需要从头指针L,开始便利单链表最终找到 i 所对应的结点,然后找到其前驱节点,在两个结点之间进行插入操作。由于没有给定、结点,所以需要我们从头指针L向后一次遍历一遍单链表来找到p结点,故此操作的时间复杂度为O(n)。
(3)指定结点前插操作:这种操作有两种方法
第一种常规的方法就是去找到这个结点对应的前驱结点,然后在两个结点之间进行插入操作,但是由于单链表的储存方式导致给定一个结点后只能向后寻找结点,所以我们就需要一个头指针L来指向头结点,然后遍历单链表,找到给定结点的前驱结点,然后在两结点之间进行插入操作。此种方法又涉及到遍历循环,所以时间复杂度依旧是O(n)。
第二种方法比较巧妙,我们不是找不到给定结点的前驱结点吗?那我们索性就不找了。直接找到他的后继节点,然后给定一个新的结点s,我们直接把给定结点的数据域和s结点的数据域进行交换,然后再将改好的s结点插入给定结点和其后继节点之间,这样就达到了“前插”的目的,这种方法没有涉及到循环,所以时间复杂度为O(1)。
4.单链表的删除
单链表的删除操作类似于插入操作,如果要删除某一个结点p,只需要找到p的前驱结点,然后将前驱结点的指针指向p后继节点的数据域,然后再调用free函数来释放结点p。
看到了寻找p的前驱结点,我们就又会涉及到单链表无法找到给定结点的前驱结点的问题。那么此时我们就可以使用3.(3)中的第二种方法,将结点p的后继结点的数据域复制到p的数据域中,然后对结点p的后继结点进行删除操作即可。
但是这种操作方法涉及到了一个问题,如果我们要删除最后一个结点,没有后继结点怎么办?那么我们就只能提高时间复杂度,用3.(3)中的第一种方法来遍历单链表,找到前驱结点进行删除给定结点的操作。
5.单链表的查找
单链表的查找分为按位查找和按值查找,我们只需要遍历一遍单链表,再写一些代码逻辑即可找到,没有什么特殊需要讲到的地方。故此处直接跳过。
四、双链表
引:下面讲到的前指针和后指针是为了使语言通透自己定义的,课本上并没有这个概念。
模型为: 前指针 数据域 后指针
1.双链表定义
在单链表的定义中,我们讲到,链表中的各个结点是由前驱结点的指针指向结点的数据域进行连接的有一条链子,双链表顾名思义有两个链子。也就是说双链表中每个结点含有两个指针域,结点的前指针域指向前驱结点,后指针指向后继结点。也就是双链。这样就可以完美的解决单链表无法找到给定结点的前驱结点的问题。
2.双链表的插入
在两个结点p(前)和q(后)之间插入一个新的结点s,那么我们就需要进行以下操作:先创建一个s结点,然后将这个结点的后指针指向 q 的前指针,再将q的前指针(原本是指向p的)指向s的后指针,再将s的前指针指向p的后指针,再将p的后指针(原本是指向q的)指向s的前指针。这样就完成了插入操作。
其他操作参考单链表的内容。(后面可能会补充)
五、循环链表
1.循环单链表:
(1)循环单链表的定义
众所周知单链表的最后一个结点的指针指向NULL,而循环链表就是将最后一个结点的指针不指向NULL取而代之的指向头结点的数据域。其他内容参考单链表的定义
(2)循环单链表的创建
与单链表的创建一样,先创建一个头指针L,再创建一个头结点,如果头结点的指针指向NULL,就改变指针,指向头结点的数据域。然后就可以用头插法或者尾插法来对这个循环链表进行添加结点的操作。
循环单链表的其他操作参考单链表。
2.循环双链表
(1)循环双链表的定义
众所周知双链表的第一个结点前指针和最后一个结点后指针的指针都指向NULL,那么,我们将头结点的前指针从指向NULL更换为指向最后一个结点的数据域,将最后一个结点的后指针从指向NULL更换为指向头结点的数据域,那么循环双链表就完成了。
(2)循环双链表的创建
与双链表的创建一样,先创建一个头指针L,再创建一个头结点(L指向头结点),如果头结点的后指针指向NULL,就改变指针,指向头结点的数据域;如果头结点的前指针指向NULL,就改变指针,指向头结点的数据域。然后就可以用头插法或者尾插法来对这个循环链表进行添加结点的操作。
循环双链表的其他操作参考单链表。、
六、静态链表
引:静态链表中的内容不是很重要,各位看个热闹就好。
1.静态链表的定义:
静态链表的创建需要在内存中开辟一大片的内存空间,这一点有点类似于顺序表的创建,而在静态链表中有很多结点,每个结点包含数据元素,以及游标(下一个结点的数据下标)。意思就是比如第一个结点存放的数据元素是5,他的游标是3,那么在静态链表中,这个结点的后继结点就是下标为3的数据元素。(这里说明的下标可以参考数组下标)。而下标为0的结点是头结点。而头结点中是不存放数据元素的。(每个结点的数据元素与游标都需要占用内存空间)
类似与链表,链表中最后一个结点的指针是指向NULL的,而静态链表中的最后一个结点的游标是-1,替代了NULL的作用,而游标的作用类似于指针。
静态链表其实就是替代了链表中的指针,在一些低级语言中,没有指针的功能。那么我们就只能用静态链表来替代链表。