数据结构分为
- 线性数据结构
- 二维数据结构
线性数据结构
线性数据结构强调的存储和顺序。
一维数组
数组特性
- 非常重要的一点,也是被前端程序员忽视的一点,数组是定长的。也就是数组的长度是固定的,因为在操作系统上来说,数组的存储必须是一段连续的空间。这也是为什么数据量一大,通常不会用数组来存储。
- 为什么JS的数组可以随意改变长度?那是JS引擎在底层实现的,当改变数组长度时,会对数组进行扩容操作,而扩容又是比较消耗性能的(因为扩容时,会新在内存中声明一个空间,再把之前的数组拷贝过去)。也就是说在声明数组的时候,可以大概给定一个长度,避免频繁的扩容操作。
- 数组的变量,指向了数组第一个元素的位置。
var a = {1,2,3,4,5}; // a 指向 1 的位置
优点:查询性能好,指定查询某个位置;在操作系统中,通过偏移查询数据性能好
缺点:
- 因为数组的空间必须是连续的,所以数组比较大的情况,当系统的空间碎片较多的时候,容易存不下;
- 因为数组的长度是固定的,所以数组的内容难以被添加和删除;
数组的操作
-
排序
排序的本质就是比较和交换,而不是比较大小。
// 比较 (其实Array.prototype.sort传递的函数,就是这个比较函数)
function compare(a, b) {
if (a > b) return true
else return false
}
// 交换
function exchange(arr, idxa, idxb) {
var temp = arr[idxa]
arr[idxa] = arr[idxb]
arr[idxb] = temp
}
冒泡排序
- 循环,比较,交换
- 每一次循环,将最大的数推到最后面
- 循环这步操作
// 循环
function sort(arr) {
if (arr == null || arr.length <= 1) return arr
for (var i = 0; i < arr.length; i++) {
// 不用比较已经比较了的位置,比较j, j+1,所以取到j—1
for (var j = 0; j < arr.length - 1 - i; j++) {
if (compare(arr[j], arr[j + 1])) {
exchange(arr, j, j + 1)
}
}
}
}
选择排序
- 第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置
- 然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾
- 循环直到全部待排序的数据元素的个数为零
// 选择排序
function choose(arr) {
for (var i = 0; i < arr.length; i++) {
var maxIndex = 0;
for(var j = 0; j < arr.length - i; j++) {
if (compare(arr[j], arr[j + 1])){
maxIndex = j
}
}
exchange(arr[maxIndex], arr[arr.length - i])
}
}
快速排序
- 简单快排
function quickSort (arr) {
if(arr == null || arr.length <= 1) return arr;
// 1. 选出一个标准位置
var location = arr[0];
// 2. 比较,比标准位置大的放一边,小的放另一边
var left = [], right = [];
// 因为 0 已经做标准位置了,所以从 1 开始循环
for (var i = 1; i < arr.length; i++) {
if (arr[i] < location) left.push(arr[i]);
else right.push(arr[i]);
};
quickSort(left);
quickSort(right);
left.push(location);
return left.concat(right);
}
简单快速排序会不断的创建新的数组,牺牲了大量的空间,但是便于理解。
- 标准快排
不创建新的数组,用数组的index作为指针,对原数组进行操作
不会做gif图 很尴尬...
// 2. 标准快速排序
function realyQuickSort (arr, begin = 0, end = arr.length) {
if(arr == null || arr.length <= 1) return arr;
if (begin = end -1) return; // 两个指针相差为1就不用比较了 挨着了
// 建立左 右指针
var left = begin, right = end;
do {
// 移动指针 当遇到左指针对应的数大于标准位置时,暂时停止一次
// 当遇到右指针小于标准位置时,暂时停止一次
// 交换两个指针对应的数组值
do left++;
while(left < right && arr[left] < arr[begin]);
do right--;
while(right > left && arr[right] > arr[begin]);
if (left < right) exchange(arr, left, right);
} while (left < right);
var exchangePoint = left == right ? right - 1 : right;
exchange(arr, begin, exchangePoint)
// 交换完一次后得到标准位置在中间的结果数组,递归操作
realyQuickSort(arr, begin, exchangePoint)
realyQuickSort(arr, exchangePoint + 1, end)
}
链表
单链表
想要传递一个链表,必须传递链表的根节点,而每一个节点都认为自己的根节点
一般讨论链表,都指单链表,双链表实现的功能,都可以用单链表实现
- 上一个对象 存着下一个对象的引用
var b = {
value: 1,
next: null,
}
var a = {
value: 2,
next: b
}
console.log(a.next === b) // true
-
链表的特点
1). 空间上不是连续的
2). 每存放一个值,都需要多开销一个引用空间
- 优点:
- 只要内存足够大,就能存的下,不用担心空间碎片的问题;
- 链表的添加和删除非常的容易;
- 缺点:
- 查询速度慢(查询某个位置)
- 链表每一个节点都需要创建一个指向next的引用,浪费了一定内存空间,当存储的数据越多时,这部分开销的内存影响越小。
function Node (value) {
this.value = value;
this.next = null;
}
var a = new Node(1);
var b = new Node(2);
var c = new Node(3);
a.next = b;
b.next = c;
console.log(a.next.value); // 2
双向链表
- 没有根节点的概念 双向链表一般不使用
- 优点: 可以双向查找,方便
- 缺点: 多开销一个引用空间,构建麻烦
function Node(val) {
this.value = value
this.next = null
this.prev = null
}
var node1 = new Node(1)
var node2 = new Node(2)
var node3 = new Node(3)
node1.next = node2
node2.prev = node1
node2.next = node3
node3.prev = node2
链表的操作
- 链表的遍历
// 循环遍历
function bian(root) {
var temp = root;
while (true) {
if (temp != null) {
console.log(temp.value);
} else {
break;
};
temp = temp.next;
}
}
// 递归遍历
function digui (root) {
if (root == null) return;
console.log(root.value);
digui(root.next);
}
一般在遍历时,都使用递归遍历。
- 链表的逆置
链表的逆置就是将链表倒转过来
/* 1. 找到链表的倒数第二个节点
* 2.倒数第一个节点就是倒数第二个节点的next
* 3. 将倒数第一个节点的next指向倒数第二个节点
* 4. 现在倒数第一个节点就是新的根节点
*/
function reverseLink(root) {
if (root.next.next == null){
root.next.next = root;
return root.next;
} else {
var res = reverseLink(root.next)
root.next.next = root
root.next = null
return res
}
}
const reverse = reverseLink(a)
栈
栈是一种后进先出的数据结构,也就是说最新添加的项最早被移出;
它是一种运算受限的线性表,只能在表头/栈顶进行插入和删除操作。
栈有栈底和栈顶。入栈是把新元素放入到栈顶的上面,成为新的栈顶;出栈之后相邻的成为新栈顶;也就是说栈里面的元素的插入和删除操作,只在栈顶进行。
小拓展:JS函数里面有作用域的概念,有GO AO 的概念,而上面有一个只供系统使用的[[scop]]属性,里面存的就是这些GO AO, 底层实现原理估计就是栈数据结构,看来什么时候得深入了解一下。
得益于JS的数组特性,JS实现栈和队列数据结构都非常的方便
var arr = []
// 栈 先入后出
class MyStack {
arr = []
// 入栈
push(val){
this.arr.push(val)
return this.arr
}
// 出栈
pop(){
return this.arr.pop()
}
}
const stack = new MyStack()
stack.push(1)
stack.push(12)
stack.push(14)
stack.push(15)
console.log(stack.pop()) //15
console.log(stack.pop()) //14
console.log(stack.push(19)) // [1, 12, 19]
console.log(stack.pop()) // 19
队列
队列是一种先进先出的数据结构。
队列在列表的末端增加项,在首端移除项。
它允许在表的首端(队列头)进行删除操作,在表的末端(队列尾)进行插入操作。
众所周知:队列是实现多任务的重要机制!
// 队列 先入先出
class Queen {
arr = []
// 入队
push (val) {
this.arr.push(val)
return this.arr
}
// 出队
pop () {
return this.arr.shift()
}
}
const queen = new Queen()
queen.push(1)
queen.push(12)
queen.push(14)
queen.push(15)
console.log(queen.pop()) // 1
console.log(queen.pop()) // 12
console.log(queen.push(19)) // [ 14, 15, 19 ]
console.log(queen.pop()) // 14
二维数据结构
二维数组
这个应该没什么好说的了,就是数组
var arr = [ [12, 34], [21, 2123], [111, 876] ]
for(var i = 0; i < arr.length; i++){
for(var j = 0; j < arr[i].length, j++){
console.log(arr[i][j])
}
}
螺旋矩阵问题
螺旋矩阵就是给定一个二维数组,需要像贪吃蛇一样绕圈往内层旋转,将这个二维数组变成一维数组。
处理这种问题 主要就是要处理边界问题,每一次读取了一层之后,下一次就要往内层移动一层。
const arr: number[][] = [
[1, 2, 3, 4],
[12, 13, 14, 5],
[11, 16, 15, 6],
[10, 9, 8, 7],
];
// 这个数组按照螺旋矩阵旋转之后,返回的数组就应该是[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16]
// 从左往右,再往下,在往上,再往右循环。
function luoxuan(arr: number[][]): number[] {
// tslint:disable-next-line: curly
if (arr.length === 0 || !arr) return [];
const result: number[] = [];
let left = 0,
right = arr[0].length - 1,
top = 0,
bottom = arr.length - 1,
direction = "right";
while (left <= right && top <= bottom) { // 相等时也需要再循环一次,否则会漏掉一层
if (direction === "right") { // 从左往右
for (let i = left; i <= right; i++) {
result.push(arr[top][i]); // 从上方左往右的过程中,高度是不变的
}
top++; // 循环完之后,相当于高度削减了一层,下一次不走这一层了
direction = "bottom";
}
if (direction === "bottom") {
for (let i = top; i <= bottom; i++) {
result.push(arr[i][right]);
}
right--;
direction = "left";
}
if (direction === "left") {
for (let i = right; i >= left; i--) {
result.push(arr[bottom][i]);
}
bottom--;
direction = "top";
}
if (direction === "top") {
for (let i = bottom; i >= top; i--) {
result.push(arr[i][left]);
}
left++;
direction = "right";
}
}
return result;
}
二维拓扑结构
二维拓扑结构,专业术语上也称图
- 二叉树,多叉树其实都是拓扑结构
// 拓扑结构
function Node (value) {
this.value = value
this.neighbor = []
}
var node1 = new Node(1)
var node2 = new Node(2)
var node3 = new Node(3)
var node4 = new Node(4)
node1.neighbor.push(node2, node3)
node2.neighbor.push(node1, node4)
node3.neighbor.push(node1, node4)
node4.neighbor.push(nod2, node3)
树形结构
- 树是图的一种
- 树有一个根节点
- 树没有环形结构(回路)
- 度: 树的最多叉的节点有多少叉,就有多少度
- 深度: 树最深有多少层,就是多少深度
在生活中树形结构的数据应用,多的不能再多了吧:dom树,目录树,关系树...
最后
数据结构,大概先记这么多,前端中这些数据结构的算法、二叉树、红黑树、斐波那契数列、动态规划啥的,
且听下回分解