前端算法系列之一:時間複雜度、空間複雜度以及數據結構棧、隊列的實現

1、此係列的原因和計劃

前段時間遇到一個實際問題怎麼最優取幣的問題,數學描述就是以下多元一次方程求解問題:前端

1x + 5y +10z + 15k + 20*j = 16 ;剛開始想着如何求解多元方程,往矩陣求解去了,結果越作越複雜,後面發現這個和揹包問題很像;而後就再重溫一下一些算法和數據結構的知識,也就有了這個系列,我計劃是把相關數據結構都一一介紹一遍,以及用JavaScript實現一遍,而後一些經典用於和實例;話很少說從最基本的開始:複雜度、棧、隊列;vue

2、複雜度

說到算法和數據結構無非就是要解決兩個問題:一、是如何更加快速準確的獲得預期結果;二、如何佔用儘量少的資源來獲得預期結果;而這兩個問題也就是咱們平時說到的性能問題,解決了這兩個問題也就解決了大部分的性能問題;那怎麼去衡量或者說去取捨這二者呢,有的時候這二者是不可兼得的,要不是爲了佔用少的資源而捨去時間的追求,要不就是爲了更快速的達到預期結果而犧牲掉必定的資源存儲,這裏涉及到空間複雜度和時間複雜度git

空間複雜度:這個就是指爲實現某個功能或者方法要佔用咱們電腦的內存資源,對於不少狀況下可能內存資源不是首要的,只要速度快能實現,好比排序中的計數排序它會要定義一箇中間數組,數組的長度是要排序數組的最大值,這樣無疑是須要更多的內存開銷的;github

時間複雜度:時間複雜度用大O來表示,雖然咱們沒法用從代碼去準確的計算執行的時間,這也是不現實由於這個時間和操做系統、硬件都有關係,因此咱們通常是有預估值來表示;通俗點就是看代碼被重複執行的次數O(n);算法

O(1): 這種狀況不論怎麼執行count方法,方法裏面++n都只會執行一次,不會隨着參數n的增長而變化,這種時間複雜度是一個常數;後端

function count (n) {  return ++n; } count(n);

O(n): 這種狀況就是隨着參數的變化,代碼被執行的次數呈現線性化的變化;api

function consoleFn(n) { 
    for(let i = 0; i < n; i++){
        console.log(i) 
    }
}

O(log2n):這種咱們稱爲對數複雜程度;像二分法查找之類的;2^i = n => 得出 i = log2n;數組

function consoleFn(n) { 
    let i = 1;
    while(i < n){
        i = i * 2; 
        console.log(i); 
    } 
}

O(nlog2n):線性對數;像快排的時間複雜度微信

function consoleFn(n) {  
    for(let j = 0; j < n; j++) { 
        let i = 1;
        while(i < n){
            i = i * 2;
            console.log(i);
        }
    }
}

O(n2):這種狀況就是執行次數會隨着n的增長而出現倍數的增長;數據結構

function consoleFn(n) {
   for(let i = 0; i < n; i++){
     for(let j = 0; j < n; j++){
      console.log(i)
     }
   }
}

微信圖片_20210115153044.jpg

數據結構:

一、棧

棧是一種聽從先進後出(FILO)原則的有序集合。新添加或待刪除的元素都保存在棧的同一端,稱做棧頂,另外一端就叫棧底。在棧裏,新元素都靠近棧頂,舊元素都接近棧底。

2-1Q201204153P8.gif

怎麼實現一個棧型數據結構呢?

首先咱們先定一下棧他的一些api:

push(val):棧頂增長一個元素;

size():返回棧的大小;

isEmpty(): 返回棧是不是空

pop():出棧

clear(): 清空棧

peek(): 返回棧頂元素

export default class Stack {
    constructor() {
        this.items = [];
    }
    push(val) {
        this.items.push(val);
    }
    size() {
        return this.items.length;
    }
    isEmpty() {
        return this.items.length === 0;
    }
    pop() {
        return this.items.pop();
    }
    peek() {
        return this.items[this.items.length - 1]
    }
    clear() {
        this.items = [];
    }
}

簡單操做:

const stack = new Stack();
console.log(stack.isEmpty()); // true
stack.push(5);
stack.push(8);
stack.push(11);
stack.push(15);
console.log(stack.isEmpty()); // false
console.log(stack.size());// 4
console.log(stack.peek());//15
stack.pop();// 15
console.log(stack.size());// 3
console.log(stack.peek());//11

微信圖片_20210117211447.jpg

思考:棧的實際應用?

vue中對模板進行解析的時候判斷模板字符是否合法就運用了棧,這和不少編輯器在書寫代碼時候校驗咱們寫的HTML元素是否閉合同樣的原理。

二、隊列;

隊列是遵循先進先出(FIFO,也稱爲先來先服務)原則的一組有序的項。隊列在尾部添加新元素,並從頂部移除元素。最新添加的元素必須排在隊列的末尾。在現實中,最多見的隊列的例子就是排隊。

20190621181828227.png

怎麼實現一個隊列數據結構呢?

首先咱們也先定一下隊列他的一些api:

enqueue(val):隊列增長一個元素;

size():返回隊列的大小;

isEmpty(): 返回隊列是不是空

dequeue():出隊列

clear(): 清空隊列

peek(): 返回隊列的首位

export default class Queue {
    constructor() {
        this.items = [];
    }
    enqueue(val) {
        this.items.push(val);
    }
    size() {
        return this.items.length;
    }
    isEmpty() {
        return this.items.length === 0;
    }
    dequeue() {
        return this.items.shift();
    }
    peek() {
        return this.items[0]
    }
    clear() {
        this.items = [];
    }
}

簡單操做:

const queue = new Queue();
queue.enqueue('a');
queue.enqueue('b');
queue.enqueue('c');
queue.enqueue('d');
console.log(queue.isEmpty()); // true
console.log(queue.size());// 4
console.log(queue.peek());// a
console.log(queue.dequeue());;// a
console.log(queue.dequeue());;// b
console.log(queue.dequeue());;// c

20180906175230552.png

隊列的應用比較常見,好比不少任務事件隊列、vue的更新隊列、vue的mixin合併隊列,都是根據先進先被執行先出隊的原則

思考:咱們在上面實現隊列和棧有沒有更好的實現方式?上面實現有什麼問題?

三、雙端隊列

雙端隊列是一種容許咱們同時從前端和後端添加和移除元素的特殊隊列。他是基於基礎隊列上的變種

image.png

api接口的定義:

addFront(element):該方法在雙端隊列前端添加新的元素。

addBack(element):該方法在雙端隊列後端添加新的元素。

removeFront():該方法會從雙端隊列前端移除第一個元素。

removeBack():該方法會從雙端隊列後端移除第一個元素。

peekFront():該方法返回雙端隊列前端的第一個元素。

peekBack():該方法返回雙端隊列後端的第一個元素。

代碼實現:

import Queue from "./QueueArr";

export class Deque extends Queue{
    constructor() {
        super();
    }
    addFront(element){
        this.items.unshift(element);
    }
    addBack(element) {
        this.enqueue(element);
    }
    removeFront() {
        return this.dequeue();
    }
    removeBack() {
        return this.items.pop();
    }
    peekFront() {
        return this.items[0];
    }
    peekBack() {
        return this.items[this.items.length - 1];
    }
}

簡單操做:

const deque = new Deque();
deque.addFront('b');
deque.addBack('c');
deque.addFront('a');
deque.addBack('d');
console.log(deque.size()) // 4
console.log(deque.peekFront());;// a
console.log(deque.peekBack());// d
console.log(deque.removeFront());//a
console.log(deque.removeBack());//d

雙端隊列的應用:

檢查迴文;

檢查一段英文是否是迴文,通常比較簡單的方法是將其轉換成數組,而後倒序一下再轉化成字符串看二者是否相同,來判斷是不是迴文;好比:ababa倒序過來仍是ababa;這就是迴文了,一樣的咱們也能夠經過使用雙端隊列來實現判斷是否是迴文;大概思路就是將字符串放進一個雙端隊列,而後分別取出隊頭和隊尾看是否相同,直到隊列出列元素小於等於1個元素,代碼實現以下:

import {Deque} from "./Deque";

export const palindromeChecked = (str) => {
    if(!str){
        return false;
    }
    const deque = new Deque();
    const strs = str.toLowerCase().split('');
    for (let i = 0; i < strs.length; i++) {
        deque.addBack(strs[i]);
    }
    let start = '', end = '', isTrue = true;
    while (deque.size() > 1 && isTrue) {
        start = deque.removeFront();
        end = deque.removeBack();
        if (start != end) {
            isTrue = false;
        }
    } 
    return isTrue
}
console.log(palindromeChecked('adfds'));// false
console.log(palindromeChecked('adada'));// true

思考:經過學習了隊列和棧,如何運用隊列和棧去實現一個四則運算,好比cal('1+2-3+6/3')獲得結果是16

部分思考解答

判斷標籤是否閉合

思路:

一、對字符串進行標籤解析

二、建立標籤棧,遇到開始標籤進行壓入棧,遇到閉合標籤進行出棧操做

三、匹配到最後,判斷棧是否爲空,若是爲空說明都閉合了,若是還有說明有標籤未能閉合

隊列和棧更好的實現

咱們上面對棧和隊列的實現都是運用了數組,對數組進行元素的刪除和增長,而對數組的操做實際上比較消耗的,像其餘靜態語言同樣,其實JavaScript對數組操做同樣是很複雜的只是js引擎幫咱們作了這些事情,例如從數組的頭部刪除或者增長一個元素,其實內部都要進行平移數組其餘元素,好比插入一個元素數組要先把長度增長,而後全部元素後移一位空出頭部再把要增長的元素放入,因此相比起來運用對象來實現會比數組更加的好

export default class Queue {
    constructor() {
        this.counter = 0;// 計數器計算隊列大小
        this.items = {}; // 隊列存儲
        this.lowestCount = 0; // 隊列頭
    }
    // 返回隊列首位
    peek() {
        return this.items[this.lowestCount];
    }
    enqueue(element) {
        this.items[this.counter] = element;
        this.counter++;
    }
    dequeue() {
        if (this.isEmpty()) {
            return undefined;
        }
        const result = this.items[this.lowestCount];
        delete this.items[this.lowestCount];
        this.lowestCount++;
        return result;
    }
    isEmpty() {
        return this.size() === 0;
    }
    size() {
        return this.counter - this.lowestCount;
    }
    clear() {
        this.counter = 0;
        this.lowestCount = 0;
        this.items = {};
    }
}

四則運算

思路:

一、實現詞法分析解析出一個詞法隊列:tokens;

二、拿到tokens進行處理,這時候定義一個數字棧,一個操做數棧

三、當遇到數字時候將數字壓入數字棧,遇到操做符時候,判斷可否執行運算,能運算就從數字棧中出棧2個數,而後操做符棧出棧一個操做符,而後作相應的操做運算,並把運算結果壓入數字棧,做爲下一次的運算數

1+2-3+6/3 => tokens = [

'{type: 'number',value:'1'},

{type: 'operator', value: '+'},

'{type: 'number',value:'2'},

{type: 'operator', value: '-'},

'{type: 'number',value:'3'},

{type: 'operator', value: '+'},

'{type: 'number',value:'6'},

{type: 'operator', value: '/'},

'{type: 'number',value:'3'},

]'

未命名繪圖.png

相關源碼實現地址

源碼點這裏


獲取第一手信息請關注個人專欄或者關注公衆號
image

相關文章
相關標籤/搜索