文章首发于:github.com/USTB-musion…前端
今天来聊一聊前端面试中很是基础的两种数据结构——「数组」和「链表」。git
先看下几个常见的面试题:github
你能够先思考一下如何回答上边的问题🤔,而后带着答案来阅览接下来的内容。面试
在聊这个问题以前,先看一下数据从逻辑结构上的分类。主要分为两类:线性表和非线性表。数组
线性表: 数据连成一条线的结构,今天要聊的链表和数组就属于这一类,除此以外还有栈,队列等。bash
非线性表: 数据之间的关系是非线性的,如树,堆,图等。数据结构
看完线性表和非线性表以后,来继续看下:ui
数组的定义:数据是一种线性表数据结构,它用一组连续的内存空间,来存储一组具备相同类型的数据。spa
因为数组在内存中是连续存放的,因此经过下标来随机访问数组中的元素效率是很是高的。但与此同时,为了保证连续性,若是想在数组中添加一个元素,须要大量地对数据进行搬移工做。同理想在数组中删除一个元素也是如此。因此咱们得出一个结论:在数组中随机访问的效率很高,可是执行添加和删除时效率低下,平均时间复杂度为O(n)。指针
链表的定义: 是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是经过链表中的指针连接次序实现的。
刚才介绍了在数组中添加和删除一个元素的效率是低下的。而链表的存储空间是不连续的,使用链表添加或者删除一个数据,咱们并不须要为了保持内存的连续性而对数据进行搬移,因此在链表中添加和删除元素是很是高效的。但万事都有两面性,正由于链表的存储空间是不连续的,想要在链表中访问一个元素时,就没法像数组同样根据首地址和下标,经过寻址公式来计算出对应的节点。而只能经过指针去依次遍历找出相应的节点。因此咱们得出一个结论:在链表中执行添加和删除操做时效率很高,而随机访问的效率很低,平均时间复杂度为O(n)。
经过前边内容的介绍,经过一个表格来直观看下两种在时间复杂度的差别:
时间复杂度 | 数组 | 链表 |
---|---|---|
添加 | O(n) | O(1) |
删除 | O(n) | O(1) |
随机访问 | O(1) | O(n) |
因此回答下链表和数组的区别:
1.内存组织方式不一样
2.添加,删除,插入的时间复杂度不一样
3.链表支持动态扩容
示例:
输入: 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;
};
复制代码
给定一个包含 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;
};
复制代码
给定一个链表,删除链表的倒数第 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;
};
复制代码