线性数据结构(一维数据结构)
一维数据结构:数组和链表
线性数据结构强调的是存储与顺序
线性数据结构之数组
数组定长
JS引擎会自动的为我们扩容,但是扩容的过程消耗 cpu,影响性能
并且数组扩容会导致,复制原先的数组,重新开辟一部分连续的空间,显然是很消耗性能的
数组a = [1, 2, 3, 4, 5];
[]表示存储地址的偏移
操作系统小知识:通过偏移查询数据性能最好
数组的特性:
- 数组的存储在物理空间上是连续的
- 底层的数组长度是不可变的
- 数组的变量指向了数组第一个元素的位置
优点:
- 查询性能好(指定查询某个位置)
缺点:
- 因为数组的存储空间必须是连续的,所以如果数组比较大,当系统的空间碎片较多的时候,容易存不下
- 因为数组的长度是固定的,所以数组的内容难以被添加和删除
线性数据结构之链表
链表的特点:
- 在空间上不是连续的,可以是跳跃的
- 没存放一个值,都要多开销一个引用空间
优点:
- 只要空间足够大,就能存的下,不用担心空间碎片的问题
- 链表的添加和删除非常的容易
缺点:
- 查询速度慢(查询某个位置,因为链表不像数组,他不是连续的)
- 链表的每一个节点都需要创建一个指向
next的引用,浪费一定的空间(当存放的数据越多的时候,这部分的开销的内存占据的空间就越少,因此适合存放大量数据)
每一个节点都认为自己是根节点
我想传递一个链表,我必须传递链表的根节点,因为每个节点没有记录谁指向了他,只记录了自己的值和自己指向的节点
var b = {
value: 2,
next: null,
};
var a = {
value: 1,
next: b,
};
console.log(a.next === b); //true
定义链表
// 创建一个链表
function Node(value) {
this.value = value;
this.next = null;
}
var a = new Node(1);
var b = new Node(2);
var c = new Node(3);
var d = new Node(4);
a.next = b;
b.next = c;
c.next = d;
d.next = null;
线性数据结构的遍历
遍历:将一个集合中的每一个元素进行获取并查看 算法必须要有严谨性判断
循环遍历链表
// 创建一个链表
function Node(value) {
this.value = value;
this.next = null;
}
var a = new Node(1);
var b = new Node(2);
var c = new Node(3);
var d = new Node(4);
a.next = b;
b.next = c;
c.next = d;
d.next = null;
function bianlink(root) {
// 传递根节点
var temp = root;
while (true) {
if (temp != null) {
console.log(temp.value);
} else {
break;
}
// 每次循环重新赋值,最后一次循环temp=null
temp = temp.next;
}
}
bianlink(a); //1 2 3 4
递归遍历链表
// 递归遍历必须有出口
function Recursion(root) {
if (root === null) return;
console.log(root.value);
Recursion(root.next);
}
Recursion(a);
链表的逆置
// 链表的逆置
function reLink(root) {
if (root.next.next == null) {
// 当前节点是倒数第二个节点
root.next.next = root; //把最后一个节点指向倒数第二个节点
return root.next; //返回新的头节点
} else {
// 其他节点,
// 1.需要改变其他节点对自己的next指向
// 2.改变自己的next指向
var res = reLink(root.next);
root.next.next = root;
root.next = null;
return res;
}
}
var newLink = reLink(a);
Recursion(newLink);
leetcode.234-回文链表
Example:
input:head = [1,2,2,1]
output: true
Example:
input:head = [1,2]
output: false
题解:遍历链表把每个节点的值放到一个栈中保存,然后我们再次遍历这个链表,每次遍历的同时出栈一个元素,出栈的元素和当前遍历的节点值相比较,不等返回false,反之为true。
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var isPalindrome = function (head) {
let stackValue = []; //一个栈
getNodeVal(head); //节点全部入栈
// 然后就是出栈然后比较pop返回出栈的数值
let res = compare(head, stackValue);
function compare(head, stack) {
if (head == null || stack.length == 0) return;
while (head) {
let val = head.val;
if (val != stack.pop()) {
return false;
} else {
head = head.next;
}
}
return true;
}
function getNodeVal(root) {
if (root == null) {
return;
}
stackValue.push(root.val);
getNodeVal(root.next);
return stackValue;
}
return res;
};
有环的链表
如下图所示:图中的链表就形成了一个闭环,这个环的交点就是节点3
下面提供了两种方法
- 方法一:Set 是否重复 根据
Set的特性,我们遍历这个链表,每次遍历节点就把这个节点放到set中,然后继续遍历节点,如果这个链表没有环,那么set中就不会有重复,否则就直接返回这个重复的节点 - 方法二:快慢指针 这个方法,怎么说你知道这个方法那就这样写就对了,你不知道就白瞎了(因为这个你要问我怎么证明我也不知道,但是就是这么写的);
- 定义两个指针,快指针
p1、慢指针p2, - 快指针一次走两步,慢指针一次走一步;
- 因为如果是一个有环的链表,那么快慢指针会在这个环上循环,并且一定会相遇,当二者相遇时,快指针回到头节点的位置,慢指针还在那个位置;(如果没有环,那么一定是快指针先遇到
null,直接返回null就 ok 了) - 随后快慢指针一起一次一步开始移动,当二者再次相遇时,相遇的节点就是所求的交点
- 定义两个指针,快指针
function ListNode(val, next) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
var a = new ListNode(1);
var b = new ListNode(2);
var c = new ListNode(3);
var d = new ListNode(4);
a.next = b;
b.next = c;
c.next = d;
// 用set判断一个链表是否有环
// 判断一个链表是不是闭环
function isCircle(head) {
// 一个单链表有环,那么我们遍历整个链表一定说死循环的,
// 不存在node.next == null的情况;
// 我们循环到一个节点,把他放入set,如果循环中遇到set有这个节点就说明有环
// 能一直遍历到null就说明无环
let set = new Set();
let bool = blink(head);
function blink(root) {
if (root == null) return;
if (set.has(root)) {
return root;
}
set.add(root);
return blink(root.next);
}
return bool ? true : false;
}
console.log("链表是否有环:", isCircle(a));
console.log("--------------------------------");
// 快慢指针法
// 判断一个链表是否有环,有环的话返回相交的节点,没有的话返回null
function getPublicNode(head) {
if (head == null || head.next == null || head.next.next == null) return null;
let p1 = head; //快指针
let p2 = head; //慢指针
// 先让他们各走一步,错开
p1 = p1.next.next;
p2 = p2.next;
while (p1 != p2) {
// 没有相遇继续走
if (p1.next == null || p1.next.next == null) {
return null; //快指针肯定先走完整个链表,如果有环不可能有null
}
p1 = p1.next.next;
p2 = p2.next;
}
// 跳出循环,已经相遇
p1 = head; //快指针回到头部
// 随后快慢指针各走一步,二者交的点就是环的交点
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
// 跳出循环,二者又相遇,相遇即所求
return p2;
}
console.log("相交节点:", getPublicNode(a));
两个链表判断是否有交点,如果有返回一个交点,没有则返回 null(多种条件分析,再上面的方法基础上讨论几种特殊场景,建议先弄懂上面的例题再看,刚开始看不太好理解)
一个有环一个无环
两个有环
function ListNode(val, next) {
this.val = val === undefined ? 0 : val;
this.next = next === undefined ? null : next;
}
// 蜗牛形
// var a1 = new ListNode(1);
// var a2 = new ListNode(1);
// var b1 = new ListNode(2);
// var b2 = new ListNode(2);
// var b3 = new ListNode(2);
// var c = new ListNode(3);
// var d = new ListNode(4);
// var e = new ListNode(5);
// // 链表1
// a1.next = a2;
// a2.next = c;
// c.next = d;
// d.next = e;
// e.next = c;
// // 链表2
// b1.next = b2;
// b2.next = b3;
// b3.next = c;
// 环形
// let head1 = new ListNode(1);
// let head2 = new ListNode(2);
// let head3 = new ListNode(3);
// let head4 = new ListNode(4);
// head1.next = head2;
// head2.next = head3;
// head3.next = head4;
// head4.next = head2;
// let headA = new ListNode("a");
// let headB = new ListNode("b");
// let headC = new ListNode("c");
// let headD = new ListNode("d");
// headA.next = headB;
// headB.next = headC;
// headC.next = head4;
// 两个无环相交链表形
// let no1 = new ListNode(1);
// let no2 = new ListNode(2);
// let no3 = new ListNode(3);
// let no4 = new ListNode(4);
// no1.next = no2;
// no2.next = no3;
// no3.next = no4;
// let y1 = new ListNode("a");
// let y2 = new ListNode("b");
// y1.next = y2;
// y2.next = no3;
// 两个链表判断是否相交
/**
* @param {ListNode} headA
* @param {ListNode} headB
* @return {ListNode}
*/
var getIntersectionNode = function (headA, headB) {
var p1 = getNode(headA); //链表A的交点
var p2 = getNode(headB); //链表B的交点
if (p1 == null && p2 == null) {
//两个链表各自没有交点,可能两个链表交叉
let n = headA;
let m = headB;
let lengthDis = 0; //记录两个链表差值
while (n.next != null && n.next.next != null) {
lengthDis++;
n = n.next;
}
// 遍历另一个链表
while (m.next != null && m.next.next != null) {
lengthDis--;
m = m.next;
}
lengthDis = Math.abs(lengthDis);
n = lengthDis > 0 ? headA : headB; //让n指向长的链表
m = n == headA ? headB : headA;
while (lengthDis > 0) {
n = n.next;
}
// 这个时候两个链表同时移动的相交点就是所求
while (n.next != null) {
if (n == m) {
return n;
}
n = n.next;
m = m.next;
}
return null;
} else if ((p1 == null && p2 != null) || (p2 == null && p1 != null))
return null;
else {
// 两个有环的链表又分为三种情况
// 情况一:两个不相交的环
if (p1 == p2) {
return p1;
} else {
// 两个交点不相等
// 任意让其中一个点再走一遍,另一个点不动,如果能遇到就说明两个相交,否则说明不相交
let end = p1;
p1 = p1.next;
while (p1 != end) {
if (p1 == p2) {
return p1;
}
p1 = p1.next;
}
return null;
}
}
// 返回一条链表的交点,没有交点就返回null
function getNode(head) {
// 为什么要这样判断,对于一条链表而言节点小于3就无法有交点
if (head == null || head.next == null || head.next.next == null)
return null;
let p1 = head; //快指针
let p2 = head; //慢指针
p1 = p1.next.next; //为了避免从同一起点开始,先让他们各动一次
p2 = p2.next;
while (p1 != p2) {
// 如果这个链表无环,那么一定是快指针先到null
if (p1.next == null || p1.next.next == null) {
return null;
}
// 继续移动
p1 = p1.next.next;
p2 = p2.next;
}
// 此时说明p1、p2在环上相遇
p1 = head; //快指针回到头部
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
// 此时这个点就是所求
return p1;
}
};
console.log(getIntersectionNode(no1, y1));
双向链表
- 优点
无论给出哪一个节点都能对整个链表实现遍历
- 缺点
要多耗费一个引用空间,而且构建双向链表比较复杂
// 实现双链表
function Node(value) {
this.value = value;
this.next = null;
this.last = null;
}
var node1 = new Node(1);
var node2 = new Node(2);
var node3 = new Node(3);
var node4 = new Node(4);
node1.next = node2;
node2.last = node1;
node2.next = node3;
node3.last = node2;
node3.next = node4;
node4.last = node3;