文章首发于:github.com/USTB-musion…
写在前面
今天来聊一聊前端面试中非常基础的两种数据结构——「数组」和「链表」。
先看下几个常见的面试题:
- 谈一谈链表和数组的区别
- 如何反转单向链表
- 在数组中找出三个数,使得它们的和为N
- 给定一个链表,删除链表的倒数第n个节点,如何实现
你可以先思考一下如何回答上边的问题🤔,然后带着答案来阅览接下来的内容。
1.链表和数组的区别
在聊这个问题之前,先看一下数据从逻辑结构上的分类。主要分为两类:线性表和非线性表。
线性表: 数据连成一条线的结构,今天要聊的链表和数组就属于这一类,除此之外还有栈,队列等。
非线性表: 数据之间的关系是非线性的,如树,堆,图等。
看完线性表和非线性表之后,来继续看下:
数组的定义:数据是一种线性表数据结构,它用一组连续的内存空间,来存储一组具有相同类型的数据。
由于数组在内存中是连续存放的,所以通过下标来随机访问数组中的元素效率是非常高的。但与此同时,为了保证连续性,如果想在数组中添加一个元素,需要大量地对数据进行搬移工作。同理想在数组中删除一个元素也是如此。所以我们得出一个结论:在数组中随机访问的效率很高,但是执行添加和删除时效率低下,平均时间复杂度为O(n)。
链表的定义: 是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
刚才介绍了在数组中添加和删除一个元素的效率是低下的。而链表的存储空间是不连续的,使用链表添加或者删除一个数据,我们并不需要为了保持内存的连续性而对数据进行搬移,所以在链表中添加和删除元素是非常高效的。但万事都有两面性,正因为链表的存储空间是不连续的,想要在链表中访问一个元素时,就无法像数组一样根据首地址和下标,通过寻址公式来计算出对应的节点。而只能通过指针去依次遍历找出相应的节点。所以我们得出一个结论:在链表中执行添加和删除操作时效率很高,而随机访问的效率很低,平均时间复杂度为O(n)。
通过前边内容的介绍,通过一个表格来直观看下两种在时间复杂度的差异:
| 时间复杂度 | 数组 | 链表 |
|---|---|---|
| 添加 | O(n) | O(1) |
| 删除 | O(n) | O(1) |
| 随机访问 | O(1) | O(n) |
所以回答下链表和数组的区别:
1.内存组织方式不同
2.添加,删除,插入的时间复杂度不同
3.链表支持动态扩容
2.如何反转单向链表
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
设置三个变量来分别表示当前节点和当前节点的前后节点,翻转后即可。
var reverseList = function(head) {
let pre = null;
for (let cur = head; cur;) {
let nextTemp = cur.next; // 保存当前头节点的下一个节点
cur.next = pre;
pre = cur;
cur = nextTemp;
}
return pre;
};
3.在数组中找出三个数,使得它们的和为N
给定一个包含 n 个整数的数组nums,判断nums中是否存在三个元素 a,b,c ,使得a + b + c = 0 ?找出所有满足条件且不重复的三元组。
例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4],
满足要求的三元组集合为:
[
[-1, 0, 1],
[-1, -1, 2]
]
1.对数组进行排序,如果数组的第一个元素大于0或者最后一个元素小于0,则不可能出现和为0的情况;
2.首先选定一个元素(A),再利用双指针对数组的元素进行遍历,将一个指针指向A元素的后一个元素(B),另一个指针指向数组的最后一个元素(C);
3.判断B+C的结果是否是A的相反数,如果B+C > (-A) ,则使C指针向前移动,如果B+C <(-A) ,则使B指针向后移动;
4.如果B指针的元素与其前一个元素相等,则B指针向后移动一位;如果C指针与其后一个元素相等,则C指针向前移动一位;
5.重复以上步骤
实现代码如下:
var threeSum = function(nums) {
//用来存取最后结果集
let result = new Array();
//头指针
let head;
//尾指针
let end;
//固定值
let fixedVal;
//排序
nums.sort((a, b) => {
return a-b;
});
//判断数组内元素是否都为整数或负数,直接返回
if(nums[0] > 0 || nums[nums.length - 1] < 0) return result;
// 开始遍历
for (let i = 0; i < nums.length; i++) {
//固定值
fixedVal = nums[i];
// 如果前后元素相同,跳过此次循环(固定值)
if(fixedVal === nums[i-1]) continue;
//一开始的固定值为nums[0],所以头指针为 i+1 下一个元素
head = i+1;
//尾指针
end = nums.length - 1;
//如果头指针小于尾指针元素
while(head < end){
//判断固定值+头指针+尾指针是否等于0
if(nums[head] + nums[end] + fixedVal === 0){
//声明数组,存放这三个值
let group = new Array();
group.push(nums[head]);
group.push(nums[end]);
group.push(fixedVal);
result.push(group);
//存放完毕之后,不要忘记头指针和尾指针的移动(否则会产生死循环)
head += 1;
end -= 1;
//如果头指针满足小于尾指针且移动后的指针和移动前的指针元素相等,再往前移动
while(head < end && nums[head] === nums[head - 1]){
head += 1;
}
//如果头指针满足小于尾指针且移动后的指针和移动前的指针元素相等,再往后移动
while(head < end && nums[end] === nums[end + 1]){
end -= 1;
}
//小于 0 需要移动头指针,因为尝试着让数据比原有数据大一点
}else if(nums[head] + nums[end] + fixedVal < 0){
head++;
}else{
//否则,尾指针向前移动,让数据小于元数据
end--;
}
}
}
return result;
};
4.给定一个链表,删除链表的倒数第n个节点,如何实现
给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。 示例:
给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.
我们可以使用两个指针而不是一个指针。第一个指针从列表的开头向前移动 n+1 步,而第二个指针将从列表的开头出发。现在,这两个指针被 n个结点分开。我们通过同时移动两个指针向前来保持这个恒定的间隔,直到第一个指针到达最后一个结点。此时第二个指针将指向从最后一个结点数起的第n个结点。我们重新链接第二个指针所引用的结点的 next 指针指向该结点的下下个结点。
var removeNthFromEnd = function(head, n) {
let first = head; // 慢指针
for (let i = 0; i < n; i++) {
first = first.next;
}
if (!first) return head.next; // 当链表长度为n时,删除第一个节点
let second = head; // 快指针
while (first.next) {
first = first.next;
second = second.next;
}
second.next = second.next.next;
return head;
};
参考资料
你可以关注我的公众号「慕晨同学」,鹅厂码农,平常记录一些鸡毛蒜皮的点滴,技术,生活,感悟,一起成长。