前端算法 - 数据结构

499 阅读9分钟

数据结构分为

  1. 线性数据结构
  2. 二维数据结构

线性数据结构

线性数据结构强调的存储和顺序。

一维数组

数组特性

  • 非常重要的一点,也是被前端程序员忽视的一点,数组是定长的。也就是数组的长度是固定的,因为在操作系统上来说,数组的存储必须是一段连续的空间。这也是为什么数据量一大,通常不会用数组来存储。
  • 为什么JS的数组可以随意改变长度?那是JS引擎在底层实现的,当改变数组长度时,会对数组进行扩容操作,而扩容又是比较消耗性能的(因为扩容时,会新在内存中声明一个空间,再把之前的数组拷贝过去)。也就是说在声明数组的时候,可以大概给定一个长度,避免频繁的扩容操作。
  • 数组的变量,指向了数组第一个元素的位置。var a = {1,2,3,4,5}; // a 指向 1 的位置

优点:查询性能好,指定查询某个位置;在操作系统中,通过偏移查询数据性能好

缺点:

  1. 因为数组的空间必须是连续的,所以数组比较大的情况,当系统的空间碎片较多的时候,容易存不下;
  2. 因为数组的长度是固定的,所以数组的内容难以被添加和删除;

数组的操作

  • 排序

    排序的本质就是比较和交换,而不是比较大小。


// 比较 (其实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)
}

链表

单链表

想要传递一个链表,必须传递链表的根节点,而每一个节点都认为自己的根节点

一般讨论链表,都指单链表,双链表实现的功能,都可以用单链表实现

  1. 上一个对象 存着下一个对象的引用
var b = {
    value: 1,
    next: null,
}
var a = {
    value: 2,
    next: b
}
console.log(a.next === b) // true
  1. 链表的特点

    1). 空间上不是连续的

    2). 每存放一个值,都需要多开销一个引用空间

  • 优点:
  1. 只要内存足够大,就能存的下,不用担心空间碎片的问题;
  2. 链表的添加和删除非常的容易;
  • 缺点:
  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

双向链表

  • 没有根节点的概念 双向链表一般不使用
  1. 优点: 可以双向查找,方便
  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

链表的操作

  1. 链表的遍历
// 循环遍历
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. 链表的逆置

链表的逆置就是将链表倒转过来

/* 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])
    }
}

螺旋矩阵问题

leetCode第54题:螺旋矩阵

螺旋矩阵就是给定一个二维数组,需要像贪吃蛇一样绕圈往内层旋转,将这个二维数组变成一维数组。

处理这种问题 主要就是要处理边界问题,每一次读取了一层之后,下一次就要往内层移动一层。

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树,目录树,关系树...

最后

数据结构,大概先记这么多,前端中这些数据结构的算法、二叉树、红黑树、斐波那契数列、动态规划啥的,

且听下回分解