对数组和链表的一些碎碎念
链表的存储结构常常和数组一起谈论,数组中的元素在内存中连续分布,而链表中的节点零散得散落在堆上。由数组和链表各自的存储结构的特点,他俩有着互补的优势和劣势。数组的连续分布性使得定位数组中指定位置上的元素十分方便,已知数组的起始地址和每个元素所占用的偏移长度,计算指定位置上的元素的地址就轻而易举:
指
定
位
置
上
的
元
素
地
址
=
起
始
地
址
+
偏
移
个
数
?
每
个
元
素
所
占
的
偏
移
长
度
指定位置上的元素地址=起始地址+偏移个数*每个元素所占的偏移长度
指定位置上的元素地址=起始地址+偏移个数?每个元素所占的偏移长度 知道所需访问的元素地址后可以轻易地访问元素的值。也正是因为内存连续性,要在数组中添加一个元素就会变得很麻烦,因为数组所在的这一“内存条”不能被你剪断,在你想插入的位置上打上“补丁”再将内存条和补丁连接这样的做法是不可能实现的。你只能将欲插入位置后面部分的每一个元素都往后挪一下,再将插入的元素放到那个位置。也即访问易,增删难
数组元素容易定位,那么对数组进行区域的划分就十分容易,我完全可以指定(index=1,index=5)这个区间为一个特定区域。而 能对一块数据的划分恰恰是分治法的基础。分治法的核心思想是划分和递归,将大问题不断拆分为小问题并逐个解决。这种思路广泛应用于查找和排序中。例如我们常用的归并排序算法就用到了分治思想,此时数组的可被划分性就显得十分重要。我们已知对两个已经排序好的数组进行合并使成为一整个有序数组是容易的,只需用两个指针分别从两个数组的头或尾出发,由这俩指针取出较小或较大的一个数据依次放入空数组中。而面对一个无序数组我们也就可以将无序分为一个个有序列表,问题的最小单元是:一个单独的元素就是一个最小的有序列表。所以归并算法就是将无序数组分割为一个个单独元素,算法的return开始起作用时就会比较有序列表首元素的值的大小再从小到大合并起来,个人理解归并的归就是return的意思,归并的并就是比较大小后合并的操作。类似的,二分查找也是在应用数组的可被划分性。
链表是由散落在内存中的一块块节点用他们身上的指针串联起来的。由于散落,所以类似数组那种一下子就能计算出某个节点的位置是不大可能了,只能从某个节点开始不断next下去。由于他们使用指针连接,所以节点的相对位置不会和物理内存耦合起来,插入一个节点只需向内存申请一个节点的内存,再将要插入位置的相邻两个节点解绑,再将插入的节点和他们串起来。也即访问难,增删易
链表操作在一些同学看来可能特别抽象。链表的增,删,反转等操作都强调先后顺序。如果添加的节点先成为了头节点的next,那么添加的节点就找不到之前头节点的next来作为自己的next了,就这么简单。
由此可见链表和数组的优缺点确实是互补的,如果能将数组和链表结合起来,这样的数据结构一定会非常有趣且高效。比方说我们在数组中存链表头,每个链表头都引出一段链表(代表一段信息链)。这样的结构可以快速定位到某个信息链,增删信息链上的数据节点也十分方便。(这要求信息链的个数不变)
如果一个单向链表的长度未知,要找到链表的倒数第N个节点就有点难了,因为访问指针的移动次数是未知的。你可以先访问一边链表得到链表长度L,再将访问指针移动
(
L
?
(
N
?
1
)
)
(L-(N-1))
(L?(N?1)) 次
这类问题包括检查链表是否有环都可以用双指针的方法解决。
找到链表的倒数第N个节点使用的是同速双指针,你可以把这个问题想象成黑夜中在甬道中行走。你和你的朋友一前一后同速行走着,一个长为N单位的竹竿被你俩一前一后扛着,你走到甬道尽头时停下,此时你的朋友感觉竹竿不动了也就停下。此时你们刚好相隔N单位。双指针法找倒数第N个节点也是一样的。front指针先移动N次,然后behind指针同时移动,每次移动一个元素,front到达最后一个节点时,behind刚好在Last-N上
对于链表是否有环则需要使用快慢指针,如果链表有环则快慢指针终有一刻会相遇。如果链表无环,则慢指针永远无法追上快指针。
有意思的是你会发现其他问题也能够转化为链表是否有环的问题。比如检查两个链表是否有相交部分。
|