JS数据结构与算法

209 阅读8分钟

排序

1.冒泡排序

第一层循环为大循环,
第二层循环为小循环,两两对比,每次对比完后,将最大值送至最后一位。
比较的次数为(N-1)+(N-2)+...+1 = N*(N-1)/2
复杂度(保留最高阶并去掉常数项)为O(N^2)

        function bubbleSort(arr) {
            for (let i = 0; i < arr.length; i++) {
                for (let j = 0; j < arr.length - i - 1; j++) {
                    if (arr[j] > arr[j + 1]) {
                        let temp = arr[j];
                        arr[j] = arr[j + 1];
                        arr[j + 1] = temp;
                    }
                }
            }
            return arr
        }
        let arr1 = [6, 5, 4, 3, 2, 1];
        console.log(bubbleSort(arr1));

2.选择排序

第一层为大循环,每次循环时将第一位索引赋值给min,
第二层循环为小循环,(打擂台的形式)将第一个的索引然后将第一个数跟其余的相比较,每次相比如果有比第一个数小的值就将其索引放入min,比完第一轮再将该数arr[min]与第一个值交换,然后又从第二个值开始循环往复。
比较次数O(N^2)
交换次数O(N)

        function selectSort(arr) {
            for (let i = 0; i < arr.length - 1; i++) {
                let min = i;
                for (let j = i + 1; j < arr.length; j++) {
                    if (arr[j] < arr[min]) {
                        min = j; //序号
                    }
                }
                if (min != i) {
                    let temp = arr[i];
                    arr[i] = arr[min];
                    arr[min] = temp;
                }
            }
            return arr;
        }
        let arr1 = [2, 4, 3, 5, 1];
        console.log(selectSort(arr1));

3.插入排序

第一层循环为大循环,默认第0位为有序,将第1位复制给temp,每次循环都将i>0赋值给temp
第二层循环为小循环,每次将temp与前面的值arr[j-1]比较,然后j--,循环比较,若temp小于该值,就一个把temp移到该值前面,如何移动?每一次小循环,将前一个大于后面的数,直接赋值给后一个数,比如5 4 3 就变成 5 5 4,然后temp是3,最后将它赋值给第j(j--)位。
比较次数(平均来说比选择排序少了一半,但是算最高次幂还是)O(N^2)

        function insertSort(arr) {
            for (let i = 1; i < arr.length; i++) {
                let temp = arr[i];
                let j;
                for (j = i; temp < arr[j - 1] && j > 0; j--) {
                    arr[j] = arr[j - 1];
                }
                arr[j] = temp;
            }
            return arr;
        }
        let arr1 = [2, 4, 3, 5, 1];
        console.log(insertSort(arr1));

4.希尔排序

原理与插入排序相似,但是插入排序是假定前面有序,然后往这个序列一个一个插入;而希尔排序是分成几个小组,对每个小组来进行插入排序
时间复杂度最坏的情况为O(N^2),但通常要好于O(N^2)

        function insertSort(arr) {
            let gap = Math.floor(arr.length / 2);
            while (gap !== 0) {
                for (let i = gap; i < arr.length; i++) {
                    let temp = arr[i];
                    let j;
                    for (j = i; temp < arr[j - gap] && j > gap - 1; j -= gap) {
                        arr[j] = arr[j - gap];
                    }
                    arr[j] = temp;
                }
                gap = Math.floor(gap / 2);
            }
            return arr;
        }
        let arr1 = [2, 4, 3, 5, 1];
        console.log(insertSort(arr1));

5.快速排序

这里直接将第一个值为基准数,然后i再左,往右走,找到大于等于基准数的就停;j在右,往左走,找到小于基准数的就停,然后分别交换值
然后就这样循环找和交换,直到不满足i < j为止
然后将基准数和中间那个找到的数交换
这样就可以得到基准数左边全是小于它的数,右边全是大于它的数
然后分而治之,左边递归,右边递归

        function quick_sort(arr, from, to) {
            var i = from; //哨兵i
            var j = to; //哨兵j
            var key = arr[from]; //标准值
            if (from >= to) { //如果数组只有一个元素
                return;
            }
            while (i < j) {
                while (arr[j] > key && i < j) { //从右边向左找第一个比key小的数,找到或者两个哨兵相碰,跳出循环
                    j--;
                }
                while (arr[i] <= key && i < j) { //从左边向右找第一个比key大的数,找到或者两个哨兵相碰,跳出循环
                    i++;
                }
                // /**
                //   代码执行道这里,1、两个哨兵到找到了目标值。2、j哨兵找到了目标值。3、两个哨兵都没找到(key是当前数组最小值)
                // **/
                if (i < j) { //交换两个元素的位置
                    var temp = arr[i];
                    arr[i] = arr[j];
                    arr[j] = temp;

                }
            }
            arr[from] = arr[i] //
            arr[i] = key;
            quick_sort(arr, from, i - 1);
            quick_sort(arr, i + 1, to);
        }

        var arr = [3, 3, -5, 6, 0, 2, -1, -1, 3];
        quick_sort(arr, 0, arr.length - 1);
        console.log(arr);

链表

指针介绍

在放代码之前首先介绍一下之后要用到的相关指针具体含义比如this.head,newnode等。
其中,指针也就是内存地址,指针变量是用来存放内存地址的变量。
1)newnodenewnode也是一个指针,所指向的节点node里面有datanextnext本身也是个指针变量,里面存放下一个node的地址
2)newnode.nextnewnode就是newnode本身这个指针,而newnode.表示要去newnode这个内存区域里找它真正存放的那个节点,newnode指向这个节点,所以newnode.next表示这个节点的nodenext;
3)this.head: 这是头节点,也是一个指针,所以this.head=newnode相当于把指针newnode的地址赋值给了this.head,所以this.head指向这个newnode
4)this.head.next: this.head若指向第一个节点node,则this.head.next就是第一个节点nodenext
5)newnode = this.head:由于this.head存了第一个节点的地址,即指向第一个节点,现把该地址赋值给newnode,相当于指newnode也指向了该节点(见下图蓝箭头);
6)newnode.next = this.head:指针newnode指向的节点的next指向了这个节点,这也是写链表各类方法所经常用到的(见下图红箭头)。

上代码~

这里采用es5的写法,利用构造函数来创建对象,并且还需要在里面再定义一个内部的类即节点类来描述有data和next的链表数据结构

function LinkedList() {
    // 内部的类:节点类
        function Node(data) {
        this.data = data
        this.next = null
    }
     // 属性
    this.head = null // 默认情况指向null
    this.length = 0 // 记录链表的长度
    // 方法
    LinkedList.prototype.append = function() {
        // ``` 见下面
    }
    LinkedList.prototype.toString = function() {
        // ``` 见下面
    }
    LinkedList.prototype.insert = function() {
        // ``` 见下面
    }
    LinkedList.prototype.removeAt = function() {
        // ``` 见下面
    }
    // ···等方法
}

append方法

LinkedList.prototype.append = function(data) {
    let newNode = new Node(data);
    if (this.length == 0) {
        this.head = newNode; // 头节点指向新节点
    } else {
        let current = this.head; 
        // this.head指向第一个节点,this.head存的地址赋值给了current,current也就指向第一个节点
        while (current.next) {
            current = current.next;
            // 假如current存的是第一个节点的地址,current.next存了下一个节点的地址,赋给current,此时current存的就是下一个节点的地址,current就指向了第二个节点。
        }
        current.next = newNode;
        // 此时current已指向最后一个节点,current.next指向新节点即可
    }
    this.length++;
}

toString方法

LinkedList.prototype.toString = function() {
    let current = this.head;
    let linkStr = '';
    while (current) {
        linkStr += current.data + ' ';
        current = current.next;
    }
    return linkStr;
}

insert方法

LinkedList.prototype.insert = function(position, data) {
    let newNode = new Node(data)
    if (position < 0 || position > this.length)
        return false;
    if (position == 0) {
        newNode.next = this.head; 
        // this.head存的是第一个节点的地址,赋值给newNode.next,所以newNode.next存的也是第一个节点的地址,所以新节点的next指向第一个节点
        this.head = newNode; 
        // 新节点地址赋值给头节点,所以头节点指向新节点
    } else {
        let cur = this.head;
        let prev = null;
        for (let i = 0; i < position; i++) {
            prev = cur; //第一个点
            cur = cur.next; //第二个点
        }
        newNode.next = cur;
        prev.next = newNode;
    }
    this.length++;
    return true
}

removeAt方法

LinkedList.prototype.removeAt = function(position) {
    if (position < 0 || position >= this.length) return false;
    if (position == 0) {
        this.head = this.head.next;
    } else {
        let cur = this.head;
        let prev = null;
        for (let i = 0; i < position; i++) {
            prev = cur;
            cur = cur.next;
        }
        prev.next = cur.next;
    }
    this.length--;
}

完整代码

function LinkedList() {
    function Node(data) {
        // 用node类来表示链表这种数据结构
        this.data = data;
        this.next = null;
    }

    this.length = 0;
    this.head = null;

LinkedList.prototype.append = function(data) {
<!--采用prototype来共享方法,节省内存-->
    let newNode = new Node(data);
    if (this.length == 0) {
        this.head = newNode;
    } else {
        let current = this.head;
        while (current.next) {
            current = current.next;
        }
        current.next = newNode;
    }
    this.length++;
}
    LinkedList.prototype.toString = function() {
        let current = this.head;
        let linkStr = '';
        while (current) {
            linkStr += current.data + ' ';
            current = current.next;
        }
        return linkStr;
    }
    LinkedList.prototype.insert = function(position, data) {
        let newNode = new Node(data)
        if (position < 0 || position > this.length)
            return false;
        if (position == 0) {
            newNode.next = this.head;
            this.head = newNode;
        } else {
            let cur = this.head;
            let prev = null;
            for (let i = 0; i < position; i++) {
                prev = cur; //第一个点
                cur = cur.next; //第二个点
            }
            newNode.next = cur;
            prev.next = newNode;
        }
        this.length++;
        return true
    }
    LinkedList.prototype.removeAt = function(position) {
        if (position < 0 || position >= this.length) return false;
        if (position == 0) {
            this.head = this.head.next;
        } else {
            let cur = this.head;
            let prev = null;
            for (let i = 0; i < position; i++) {
                prev = cur;
                cur = cur.next;
            }
            prev.next = cur.next;
        }
        this.length--;
    }
}
let linkList = new LinkedList();
linkList.append('111')
linkList.append('222')
linkList.append('333')
linkList.insert(3, '444')
linkList.removeAt(2)
console.log(linkList.toString());

二叉树

一个二叉树第 i 层的最大节点数:2^{(i-1)}
深度为 i 的二叉树有最大节点总数:2^i-1

1. 完美二叉树

高度为 i,并且由2^{(i-1)}个结点组成的二叉树,称为完美二叉树。

2. 完全二叉树

一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下层的叶结点集中在靠左的若干位置上,这样的二叉树称为完全二叉树。

3. 二叉搜索树

相关代码:

    function Node(data) {
        this.data = data;
        this.left = null;
        this.right = null;
    }

    function BST() {
        this.root = null;
    }
   
    // 方法
    BST.prototype.insert = function() {
        // ``` 见下面
    }
    BST.prototype.preOrder = function() {
        // ``` 见下面
    }
    BST.prototype.midOrder = function() {
        // ``` 见下面
    }
    BST.prototype.postOrder = function() {
        // ``` 见下面
    }
    // ···等方法

insert方法

BST.prototype.insert = function(data) {
    let newNode = new Node(data);
    if (this.root == null) {
        this.root = newNode;
    } else {
        this.insertNode(this.root, newNode);
    }
}

BST.prototype.insertNode = function(node, newNode) {
    if (newNode.data < node.data) {
        if (node.left != null) {
            this.insertNode(node.left, newNode);
        } else {
            node.left = newNode;
        }
    } else {
        if (node.right != null) {
            this.insertNode(node.right, newNode)
        } else {
            node.right = newNode;
        }
    }
}

先序遍历

左节点相关代码还没执行就打印

BST.prototype.preOrder = function(handler) {
    this.preOrderTraversalNode(this.root, handler)
}
BST.prototype.preOrderTraversalNode = function(node, handler) {
    if (node) {
        handler(node.data);
        this.preOrderTraversalNode(node.left, handler);
        this.preOrderTraversalNode(node.right, handler);
    }
}

// 测试数据
    var bst = new BST();
    var nums = [5, 3, 8, 1, 4, 7, 9];
    for (var i = 0; i < nums.length; i++) {
        bst.insert(nums[i]);
    }
    let str = '';
    bst.preOrder(res => {
        str += res + ' '
    })
    console.log(str); // 5 3 1 4 8 7 9

中序遍历

左节点相关代码执行完了就打印

    BST.prototype.midOrder = function(handler) {
        this.midOrderTraversalNode(this.root, handler)
    }
    BST.prototype.midOrderTraversalNode = function(node, handler) {
        if (node) {
            this.midOrderTraversalNode(node.left, handler);
            handler(node.data);
            this.midOrderTraversalNode(node.right, handler);
        }
    }
    
    bst.midOrder(res => {
        str += res + ' '
    })
    console.log(str); // 1 3 4 5 7 8 9 相当于按顺序

后续遍历

左节点右节点相关代码执行完了再打印

    BST.prototype.postOrder = function(handler) {
        this.postOrderTraversalNode(this.root, handler);
    }
    BST.prototype.postOrderTraversalNode = function(node, handler) {
        if (node) {
            this.postOrderTraversalNode(node.left, handler);
            this.postOrderTraversalNode(node.right, handler);
            handler(node.data);
        }
    }
    
        bst.postOrder(res => {
        str += res + ' '
    })
    console.log(str); // 1 4 3 7 9 8 5

面试其他题

笔试
小明去买口罩,A类2个2元,B类3个2元,C类1个3元,D类5个1元,E类4个5元,F类3个2元,每类口罩限购一个,问小明最多能买多少个口罩?
输入:9
输出:13
这本质上是01背包问题
采用动态规划