线性表
1.什么是线性表
线性表是 n 个数据元素的有限序列,最常用的是链式表达,通常也叫作线性链表或者链表。在链表中存储的数据元素也叫作结点,一个结点存储的就是一条数据记录。每个结点的结构包括两个部分:
-
第一是具体的数据值;
-
第二是指向下一个结点的指针。
仔细观察上图,你会发现这个链表只能通过上一个结点的指针找到下一个结点,反过来则是行不通的。因此,这样的链表也被称作单向链表。
有时候为了弥补单向链表的不足,我们可以对结点的结构进行改造:
对于一个单向链表,让最后一个元素的指针指向第一个元素,就得到了循环链表;
或者把结点的结构进行改造,除了有指向下一个结点的指针以外,再增加一个指向上一个结点的指针。这样就得到了双向链表。
同样的,还可以对双向链表和循环链表进行融合,就得到了双向循环链表,如下图所示:
2.线性表的增删查
我们主要介绍单向链表的增删查操作
新增
s.next = p.next;
p.next = s;
删除
p.next = p.next.next;
查
只能遍历查询
总结
链表在新增、删除数据都比较容易,可以在 O(1) 的时间复杂度内完成。但对于查找,不管是按照位置的查找还是按照数值条件的查找,都需要对全部数据进行遍历。这显然就是 O(n) 的时间复杂度。
虽然链表在新增和删除数据上有优势,但仔细思考就会发现,这个优势并不实用。这主要是因为,在新增数据时,通常会伴随一个查找的动作。例如,在第五个结点后,新增一个新的数据结点,那么执行的操作就包含两个步骤:
第一步,查找第五个结点;
第二步,再新增一个数据结点。整体的复杂度就是 O(n) + O(1)。
线性表真正的价值在于,它对数据的存储方式是按照顺序的存储。如果数据的元素个数不确定,且需要经常进行数据的新增和删除时,那么链表会比较合适。如果数据元素大小确定,删除插入的操作并不多,那么数组可能更适合些。
3.线性表案例
例 1 链表的翻转。给定一个链表,输出翻转后的链表。例如,输入1 ->2 -> 3 -> 4 ->5,输出 5 -> 4 -> 3 -> 2 -> 1。
对于某个单向链表,它的指针结构造成了它的数据通路有去无回,一旦修改了某个指针,后面的数据就会造成失联的状态。为了解决这个问题,我们需要构造三个指针 prev、curr 和 next,对当前结点、以及它之前和之后的结点进行缓存,再完成翻转动作。
注:图片转正网络,侵删
while(curr){
/*
从前往后遍历链表,定义三个指针分别指向相邻的三个结点,反转前两个结点,即让第二个结点指向第一个结点。然后依次往后移动指针,直到第二个结点为空结束,再处理链表头尾即可。
*/
next = curr.next; // 存储curr的下一个节点地址
curr.next = prev; // 把prev地址赋值给curr的下一个节点,即指针方向反转
prev = curr; // 把curr地址赋值给prev,即prev位置后移
curr = next; // 把next(之前curr的next)赋值给curr,即curr后移
}
例 2 给定一个奇数个元素的链表,查找出这个链表中间位置的结点的数值。
这个问题也是利用了链表的长度无法直接获取的不足做文章,解决办法如下:
-
一个暴力的办法是,先通过一次遍历去计算链表的长度,这样我们就知道了链表中间位置是第几个。接着再通过一次遍历去查找这个位置的数值。
-
除此之外,还有一个巧妙的办法,就是利用快慢指针进行处理。其中快指针每次循环向后跳转两次,而慢指针每次向后跳转一次。如下图所示。
注:图片转正网络,侵删
while(fast && fast.next && fast.next.next){
fast = fast.next.next;
slow = slow.next;
}
例 3 判断链表是否有环。如下图所示,这就是一个有环的链表。
假设链表有环,快指针每次走两格,而慢指针每次走一格,相对而言,快指针每次循环会多走一步。这就意味着:
-
如果链表存在环,快指针和慢指针一定会在环内相遇,即 fast == slow 的情况一定会发生。
-
反之,则最终会完成循环,二者从未相遇。
如下图所示: