常见数据结构与算法-JavaScript版

308 阅读9分钟

1.时间复杂度与空间复杂度

时间复杂度:一个用来描述算法运行时间的函数。

空间复杂度:算法在运行过程中临时占用存储空间大小的量度。

常见时间复杂度耗时排序:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

2.链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表的常见算法有:单链表的插入与删除、反转链表、循环链表(环形链表)、双向链表。

单链表的数据结构在JavaScript中可以表示为:

//链表中单个节点的构造函数
function ListNode(val){
    this.val = val;
    this.next = null;
}
//单向链表的构造函数
function SingleLink(){
    this.head = null;
    this.length = 0;
}
//向链表末尾添加一个元素的append方法
SingleLink.prototype.append= function(ele){
    var node = new ListNode(ele);
    if(this.head == null){
        this.head = node;
    }else{
        var n = this.head;
        //找到最后一个节点,其next为null
        while(n.next){
            n = n.next;
        }
        n.next = node;
    }
    this.length++;
    return this.head;
}
//向链表任意位置添加一个元素的insert方法
SingleLink.prototype.insert = function (ele, index) {
    var node = new ListNode(ele);
    if (index > this.length) {
        return false;
    } else if (this.head == null) {
        this.head = node;
    } else if (index == 0) {
        node.next = this.head;
        this.head = node;
    } else {
        let n = this.head;
        let i = 0;
        //找到要插入位置的前一个节点
        while (i < index - 1) {
            n = n.next;
            i++;
        }
        node.next = n.next;
        n.next = node;
    }
    this.length++;
    return this.head;
}
//删除指定位置的节点的delete方法
SingleLink.prototype.delete = function (index) {
    if (index >= this.length) {
        return false;
    } else if (index == 0) {
        this.head = this.head.next;
    } else {
        let i = 0;
        let n = this.head;
        //找到要删除位置的前一个节点
        while (i < index - 1) {
            n = n.next;
            i++;
        }
        n.next = n.next.next;
    }
    this.length--;
    return this.head;
}
//获取指定位置的节点的getIndexOf方法
SingleLink.prototype.getIndexOf = function (index) {
    let node;
    if (index >= this.length) {
        return false;
    } else if (index == 0) {
        node = this.head;
    } else {
        let i = 0;
        let n = this.head;
        while (i < index) {
            n = n.next;
            i++;
        }
        node = n;;
    }
    return node;
}

此外还应该有获取链表长度的length方法和判断链表是否为空的isEmpty方法,这两个方法根据链表的length属性可以非常容易实现,这里不再赘述。

(1)删除链表的倒数第N个节点

//给定一个链表,删除链表的倒数第n个结点,并返回头结点
    /**
     * Definition for singly-linked list.
     * function ListNode(val) {
     *     this.val = val;
     *     this.next = null;
     * }
     */
    /**
     * @param {ListNode} head
     * @param {number} n
     * @return {ListNode}
     */
    var removeNthFromEnd = function (head, n) {
        let temp = [];
        //将链表转换为数组进行存储,方便获取链表长度
        while (head) {
            temp.push(head);
            head = head.next;
        }
        //获取要删除的节点位置
        let index = temp.length - n;
        //长度为1时,直接返回null
        if (temp.length === 1) {
            return null;
            //删除的节点不是最后一个
        } else if (index + 1 < temp.length) {
            //删除当前节点,返回头节点
            temp[index].val = temp[index + 1].val;
            temp[index].next = temp[index + 1].next;
            return temp[0];
            //删除的节点是最后一个
        } else {
            //删除最后一个节点,并将前一个节点置空
            temp[index - 1].next = null;
            return temp[0];
        }
    }

(2)反转链表

   //反转一个单链表 
    /**
     * @param {ListNode} head
     * @return {ListNode}
     */
    var reverseList = function (head) {
        //直接修改node的next属性会改变原链表,造成死循环,借助数组或者new一个node来深度赋值
        if (head) {
            let result = new ListNode(head.val);
            //通过while循环,不断将后边的节点作为头节点,直到head.next为空
            while (head.next) {
                //借助一个new的节点来保存之前的节点数据
                let preNode = new ListNode(result.val);
                preNode.next = result.next;
                result.val = head.next.val;
                result.next = preNode;
                head = head.next;
            }
            return result;
        } else {
            return head;
        }
    }

可以将while循环的内容改为递归调用的函数,也可以采用将链表保存成一个数组,再倒序生成链表的方式。

(3)回文链表

    //判断一个链表是否为回文链表。(空间复杂度为常数阶O(1),时间复杂度为O(n))
    //空间复杂度为O(1)可以先遍历得到链表长度,再反转前半部分,再与后半部分比较
    /**
     * @param {ListNode} head
     * @return {boolean}
     */
    var isPalindrome = function (head) {
        let length = 0;
        let i = 0;
        //获取链表的长度length
        for (let cur = head; cur != null; cur = cur.next) {
            length++;
        }
        //链表为空或长度为1,为回文链表
        if (length <= 1) {
            return true;
            //链表长度为2
        } else if (length == 2) {
            return head.val == head.next.val;
        } else {
            //将链表划分为left,right两部分
            let left = new ListNode(head.val);
            //将左半部分链表反转
            while (i < Math.floor(length / 2) - 1) {
                let preNode = new ListNode(left.val);
                preNode.next = left.next;
                left.val = head.next.val;
                left.next = preNode;
                head = head.next;
                i++;
            }
            //若链表长度为偶数,则right的头节点为head.next,否则跳过一个节点为head.next.next
            let right = parseInt(length / 2) === length / 2 ? head.next : head.next.next;
            //比较left和right链表
            while (left && right) {
                if (left.val != right.val) {
                    return false;
                } else {
                    left = left.next;
                    right = right.next;
                }
            }
            return true;
        }
    }

空间复杂度为O(n)可以先遍历得到链表数组,再遍历数组从数据头尾两个方向比较。

(4)环形链表

    //给定一个链表,判断链表中是否有环。
    /**
     * @param {ListNode} head
     * @return {boolean}
     */
    var hasCycle = function (head) {
        //链表中经常使用双指针法
        //若链表中不存在环,slow和fase步调不同,永不指向同一个对象
        //反之,若存在环,则终有指向同一个对象之时
        let slow = head;
        let fast = head;
        while (fast && fast.next) {
            slow = slow.next;
            fast = fast.next.next;
            if (slow == fast) return true;
        }
        return false;
    }
    //////////////////////////////////////////////////////
    var hasCycle = function (head) {
        //将链表存储为数组,若新push进的对象与之前存储的对象相同,则存在环
        //若不存在环,则链表终会遍历完,不会造成死循环
        let arr = [];
        while (head) {
            arr.push(head);
            for (let i = 0; i < arr.length; i++) {
                if (head.next == arr[i]) {
                    return true;
                }
            }
            head = head.next;
        }
        return false;
    };

链表种使用双指针法有效降低了空间复杂度,为O(1),时间复杂度为O(n)。而使用数组的解法,空间复杂度为O(n),时间复杂度为O(n2)。

3.栈

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,把另一端称为栈底。栈的应用包括:递归、四则运算表达式求值、进制数转换等。

栈的数据结构在JavaScript中可以表示为:

//栈中单个元素的构造函数function StackNode(val) { 
   this.val = val;
   this.next = null;
}
//栈的构造函数
function Stack() {
    this.top = null;
    this.length = 0;
}
//入栈方法
pushStack.prototype.push = function (ele) {
    var node = new StackNode(ele);
    if (this.top == null) {
        this.top = node;
    } else {
        node.next = this.top;
        this.top = node;
    }
    this.length++;
    return this;
}
//出栈方法
popStack.prototype.pop = function () {
    if (this.length == 0) {
        return false;
    } else {
        var temp = this.top.next;
        this.top = temp;
    }
    this.length--;
    return this;
}

以上代码实现了栈的链式结构存储。与链表类似,top是栈的栈顶元素,相当于链表的头节点。每次push都会在栈顶加入一个元素,即在链表的头节点前再加一个节点。pop即删除头节点,栈顶元素变为头节点的下一个节点。

JavaScript中栈的实现常用数组Array,Array自带push\pop\length等方法,在此不再实现。

此外还应有栈的length方法和clear方法,不再赘述。

(1)最小栈

/*设计一个支持 push,pop,top 操作,并能在常数时间内检索到最小元素的栈。

push(x) -- 将元素 x 推入栈中。
pop() -- 删除栈顶的元素。
top() -- 获取栈顶元素。
getMin() -- 检索栈中的最小元素。*/
   /**
     * initialize your data structure here.
     */
    var MinStack = function () {
        this.items = [];
        this.min = [];
        this.p = null;
    };

    /** 
     * @param {number} x
     * @return {void}
     */
    MinStack.prototype.push = function (x) {
        this.items.push(x);
        this.p = x;
        if (this.min.length == 0) {
            this.min.push(x);
        } else {
            //min中存储直到栈中push进x为止,最小的元素,加等号,相同的最小元素
            let l = this.min.length;
            this.min[l - 1] >= x ? this.min.push(x) : '';
        }
    };

    /**
     * @return {void}
     */
    MinStack.prototype.pop = function () {
        let len = this.items.length;
        let minLen = this.min.length;
        //当最小的元素被pop出去时,min也pop出最小元素,
        //则min中最后一个为截止到剩下的元素为止,它们中的最小值
        if (this.items[len - 1] == this.min[minLen - 1]) {
            this.min.pop();
        }
        this.items.pop();
        this.p = this.items[len - 2];
    };

    /**
     * @return {number}
     */
    MinStack.prototype.top = function () {
        return this.p;
    };

    /**
     * @return {number}
     */
    MinStack.prototype.getMin = function () {
        let minLen = this.min.length;
        return this.min[minLen - 1];
    };

最小栈的难点在于如何在常数时间内找到最小值,如果之前未对push进栈中的元素进行相关记录的话,则找到最小值最少也需要O(n)的时间复杂度。

声明一个数组,第一项为栈底元素(默认最小值),在每次push一个新数进栈时,与数组的最后一项比较,如果比这一项小,则push进该数组,表示到该元素进栈为止,最小值是数组最后一项,数组长度+1。如果不比这一项小,则表示到该元素进栈为止,最小的仍然是数组最后一项,数组长度不变。在元素出栈pop时,如果刚好pop出的是最小值,则将数组的最后一项也pop出,代表到栈中元素pop之后时,最小值发生了改变。

(2)逆波兰表达式求值(四则运算)

/*根据逆波兰表示法,求表达式的值。
有效的运算符包括 +, -, *, / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。*/
/**
 * @param {string[]} tokens
 * @return {number}
 */
//JavaScript数组实现栈结构  
      function Stack() {
            this.items = [];
            this.top = null;
            this.length = 0;
        }
        Stack.prototype.push = function (ele) {
            this.items.push(ele);
            this.top = ele;
            this.length++;
        }
        Stack.prototype.pop = function () {
            let len = this.items.length;
            this.items.pop();
            this.top = this.items[len - 2];
            this.length--;
        }
    //逆波兰式求值    
    var evalRPN = function (tokens) {
        var sta = new Stack();
        for (let i = 0; i < tokens.length; i++) {
            if (isNaN(tokens[i])) {
                let num1 = parseInt(sta.top);
                sta.pop();
                let num2 = parseInt(sta.top);
                sta.pop();
                let t;
                switch (tokens[i]) {
                    case "+":
                        t = num2 + num1;
                        break;
                    case "-":
                        t = num2 - num1;
                        break;
                    case "*":
                        t = num2 * num1;
                        break;
                    case "/":
                        t = parseInt(num2 / num1);
                        break;
                }
                sta.push(t);
            } else {
                sta.push(tokens[i]);
            }
        }
        return sta.top;
    };

将后缀表达式利用栈的先进后出计算得出结果,规则是从左至右遍历后缀表达式,如果是数字就直接进栈,如果是符号则将栈顶两个元素出栈,进行计算,再将计算结果入栈。

将常见的中缀表达式转为后缀表达式也是利用栈的先进后出。规则是从左至右遍历中缀表达式,如果式数字就直接输出,如果符号就比较与栈顶符号的优先级,是右括号或优先级低于栈顶符号,则栈顶元素依次输出并出栈,并将当前符号进栈,一直到最后输出后缀表达式。

(3)递归-斐波那契数列

/*斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。
该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。
给定 N,计算 F(N)。*/
/**
 * @param {number} N
 * @return {number}
 */
var fib = function(N) {
    if(N<=1) return N;
    return fib(N-1)+fib(N-2)
}

(4)递归-爬楼梯

/*假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?*/
/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function(n) {
    if(n<=2) return n;
    return climbStairs(n-1)+climbStairs(n-2);
///////////使用动态规划思路////////////////
    var result = new Array();
    result = [0,1,2];
    for(var i=3;i<=n;i++){
       result[i] = result[i-1]+result[i-2]; 
    }
    return result[n];
}

爬楼梯问题适合从上向下的解决问题,当爬上最后一阶时有两种可能,一是最后一步爬一阶,二是最后一步爬两阶。同理,当爬到倒数第二阶时,也有两种可能。故可以得出f(n)=f(n-1)+f(n-2)[n>2]。

爬楼梯问题更符合动态规划类问题的解题思路,之后再续。