【数据结构】顺序表和链表
今天偶然间发现了之前考研电脑的文档, 我想看一看吧,一看我人都傻了,欺负我这个半年没学习数据结构的考研er,这好吗,这不好.于是就想把之前的东西给整理下,以后方便复习.
线性表Linear List
线性表(linear list)是n个具有相同特性的数据元素的有限序列。
线性表是一种在实际中广泛使用的数据结构,
常见的线性表:顺序表、链表、栈、队列、字符串...
顺序表Contiguous List
定义
线性表的顺序存储称为顺序表,用一组地址连续的存储单元依次存储线性表中的数据元素。
即顺序表的两元素不仅逻辑相邻,物理位置也相邻。逻辑顺序与物理顺序相同
在大学学习的过程中使用最多的就是c语言中的数组了吧!
特点
支持随机访问
(即通过元素序号和首地址下标在O(1)时间复杂度内找到该元素)存储密度高
(即每个节点都存储数据元素)插入和删除需要移动大量元素
(因为顺序表逻辑上相邻的元素物理上也相邻,修改一个元素则需要变更很多)
顺序表考查角度
解题一般使用暴力解法
枚举法
枚举法是最容易想到的方法,把所有可能的情况都考虑到,然后从中选出符合题目要求的情况,通常使用for循环遍历。
快速排序
对于一个无序的数组可以先通过排序把他变成有序再处理,排序使用快速排序。考试中直接默写快速排序,然后注意调用快速排序时的参数即可。
快排思想
快速排序是一种分治思想,每一轮快排分为两个步骤:
①选择枢值key。
②枢值移动到他的最终位置,左右划分为两个区间,左边区间的所有元素都小于枢值,右边区间的所有元素都大于枢值。
然后对左右区间分别进行快排,不断重复直到当前处理区间元素个数小于等于1(即L>=R)。
快排时间空间复杂度
快速排序的平均时间复杂度是O(nlogn),平均空间复杂度是O(logn),是考试中最快的不稳定排序算法,一般要用到排序时都使用快速排序。快速排序的最坏时间复杂度是O(n2),最坏空间复杂度都是O(logn),但我们只需要加一个小优化就能避免最坏情况:即随机选择一个元素作为枢值。优化后最坏时间复杂度O(nlogn),最坏空间复杂度O(logn)。
快排Pseudocode
1 | void Qsort(int A[], L, R){ //a数组保存数据,L和R是边界 |
如果我们不满足于暴力解法,就应该想,暴力解法浪费了什么,我们在什么地方可以对他优化,优化一般要使时间或者空间复杂度减小(主要是时间复杂度),比如说题目给的是有序数组,但是我们没有用到有序性,这是有条件没用;或者题目不需要排序,只要求中位数,但是我们对他排序了,超额完成任务,本来可以不做这么多的,这就是浪费。最好的情况就是我们完美的利用了题目的条件,而且又不多做一丁点事,这样的算法一般都更优秀。
折半查找(利用有序性)
思想:
折半查找,也叫二分查找,假设我们在升序数组A[L~R]中查找x,L和R是上下界(即Left和Right),
mid=(L+R)/2,
每次把x与A[mid]比较:
如果x>A[mid]
,说明x一定不会出现在A[Lmid],只可能出现在A[mid+1R],更新L=mid+1;
而如果x<=A[mid]
,说明x一定不会出现在A[mid+1R],只可能出现在A[Lmid],更新R=mid。
折半查找Pseudocode
1 | int Binary_Search(int A[], L, R, x){ //A[]和x不一定是int型 |
设置多指针后移
例题:
定义距离d = |x – y|,给定2个长度为n的升序数组A、B,x是数组A中的某个元素,y是数组B中的某个元素。请设计一个尽可能高效的算法,计算并输出所有可能的的最小距离D。例如 数组A = {–16, –8, 5, 8, 13},B ={-2, 0, 2, 6, 10},则最小距离为1,相应的x=5, y=6, d= |5 – 6|=1。
这是2个升序数组,查找一种最小的情况,可以考虑使用指针后移,设置两个指针(实际上是两个int变量)i和j保存此时正在处理的数组A、B元素的下标,每次只比较A[i]和B[j],数组A和B都是升序,每次i或j后移都会导致x或y变大,最终要求的是最小的d,所以我们的每次选择i还是j后移的时候原则就是尽量不让d变大,即要让A[i]和b[j]中较小的那个后移。初始时i=j=0, x=-16, y=-2, d=14,A[i]小,应该i后移即i++,这样会缩小x和y的差距,而如果j后移会继续拉大x和y的差距。①如果i++,则x=-8, y=-2,d=6;②如果j++,则x=-16, y=-0,d=16。我们的目的是求出最小的d,所以②这种情况是没意义的,选①情况,每次比较完x和y后把较小的那个值指针后移。最终每一个数组都只会遍历一次,不会回头,所以时间复杂度是O(n)。
链表Linked List
定义
顺序表和链表都是线性表的一种。但链表与顺序表不同的是,他的物理上与逻辑上的机构并不一致。
顺序表的话,逻辑相邻,物理上也是相邻的。所以对于一整块连续的物理地址,当我们进行插入和删除操作的时候就会需要大量的移动元素。
而链表不需要使用地址连续的存储单元,即不需要满足物理与逻辑一致,通过”链”建立数据之间的逻辑关系,当进行插入或删除操作的时候,只需要修改指针即可
特点
是线性表。
线性表能用的方法他都能用,比如归并两个有序序列,所以数组指针后移的方法也可以用到链表中。无法随机访问。
如果一个方法需要利用随机访问的特性,那就不能用在链表上,比如说折半查找和快速排序。只能向后查找,无法回头。
比如要求倒数第k个结点,无法从最后一个结点往前查,得转化成正数第x个结点,从前往后查,或者采用前后指针固定间距。
链表考察角度
单链表定义Pseudocode
单链表(data是该节点的权值,一般可以定义成int;next是指向下一个节点的指针,保存的是下一个节点的位置)
1 | typedef struct LNode{ |
双链表定义Pseudocode
双链表(不需要特别记忆,只是比单链表多一个prior指针而已)
1 | typedef struct LNode{ |
暴力解法
枚举法,直接处理
链表的一些题目可以先采用最直接易想的方法,比如要改变链表元素顺序的话,可以将需要重排的元素一个一个拆下来重新插入。这些方法一般看起来比较憨,但是容易想到。
将链表用数组保存,再处理
可以使用数组保存链表中的结点地址或者直接保存数据,然后再按照数组的做题方法操作即可。注意:①如果题目中求的是处理之后的链表;②如果题目要求空间复杂度为O(1);这两种情况都不能使用数组保存链表的结点。
链表的优化算法
设置多指针后移
类似于前面的数组多指针后移,在链表中也可以使用这种方法,在数组中每次后移是i++,而链表中则是p=p->next,本质上都是一样的。
例题:
已知两个长度分别为m和n带头节点的升序链表L1和L2,若将它们合并为一个长度为m + n的升序链表,头节点为L3。请设计一个时间和空间上尽可能高效的算法,返回新的头节点L1。要求:
(1)给出算法的基本设计思想。
(2)根据设计思想,采用C或C++语言描述算法,关键之处给出注释。
(3)说明你所设计算法的时间复杂度和空间复杂度。
这是2个升序链表,要合并这两个链表得到新的升序链表,可以考虑使用指针后移,设置两个指针p和q分别保存此时正在处理的链表L1和L2中的结点,每次比较p和q所指结点的权值,把权值较小的那个结点取下来尾插入新的链中,然后对应指针后移一个结点,重复进行直到一条链遍历完,然后把另一条链连在L3末尾,得到的L3就是升序序列。最终每个结点都只会遍历一次,不会回头,所以时间复杂度是O(n+m)。
链表基本操作
取出/删除p指向的结点
有三个节点的简单链表: pre -> p -> post
1 | //取出p指向的结点: |
将p结点插入到pre结点后面
有2个节点的简单链表: pre -> post
1 | //p插入pre后面的结点: |
头插法插入p
有2个节点的简单链表: L -> post, 需要将p插入到L和post之间
1 | p->next=post; |
遍历链表
对于线性表(数组+链表)的遍历,如果表中元素个数确定就用for循环,否则用while循环。
在遍历过程中访问指针p每次都需要后移即p=p->next,不要忘记。
1 | //已知元素个数n: |