數據結構和算法基礎(偏向前端方向)

提起算法不少CS畢業的人都不會陌生,可是無論你是在學校理論知識學的如何紮實仍是在學校中有參加比賽的經歷(ACM等),可是到了工做中由於沒有實際的應用場景或者說應用場景不多,致使一些本來順手拈來的知識點和操做都感到很生疏。javascript

同時,因爲本人如今專職於前端工做(原來是先後端都作),不少的應用場景都沒有涉及算法的身影。這也是前端在技術鄙視鏈的頂端。其實前端也是有不少頂級的算法思路和實現。只是別人幫你實現了,你直接就用。前端

因此我始終相信一個信條:java

實踐出真知,溫故而知新node

因此,接下來的文檔,將一塊兒彙總一下我跟着極客時間數據結構與算法之美的課程來一塊兒回顧和總結一下(內容大部分是極客時間的,可是有一些也是我查詢相關資料來進行比對和校驗的)。c++

該課程只是我的的學習文檔和看法,若有不對和理解有誤差的地方,請不吝賜教。算法

複雜度

漸進複雜度,包括時間複雜度空間複雜度,用來分析算法執行效率與數據規模之間的增加關係,能夠粗略地表示,越高階複雜度的算法,執行效率越低後端

從低階到高階有:O(1)、O(logn)、O(n)、O(nlogn)、O(n2 )數組

數組

說到數組你們想必在開發中,是接觸最多的一類數量類型,也是最多見的一類線性表瀏覽器

線性表的分類 bash

靜態類型語言(java,c++)

一種線性表數據結構。它用一組連續的內存空間,來存儲一組具備相同類型的數據。

動態類型語言(javaScript)

As JavaScript arrays are implemented as hash-maps(哈希表) or dictionaries(字典) and not contiguous.(非連續)

鏈表

它並不須要一塊連續的內存空間,它經過指針將一組零散的內存塊串聯起來使用

內存分配

鏈表經過指針將一組零散的內存塊串聯在一塊兒。其中,咱們把內存塊稱爲鏈表的結點

單鏈表

其中有兩個結點是比較特殊的,它們分別是第一個結點和最後一個結點。咱們習慣性地把第一個結點叫做頭結點,把最後一個結點叫做尾結點。其中,頭結點用來記錄鏈表的基地址。有了它,咱們就能夠遍歷獲得整條鏈表。而尾結點特殊的地方是:指針不是指向下一個結點,而是指向一個空地址 NULL,表示這是鏈表上最後一個結點。

循環鏈表

循環鏈表的尾結點指針是指向鏈表的頭結點

雙向鏈表

雙向鏈表須要額外的兩個空間來存儲後繼結點前驅結點的地址。因此,若是存儲一樣多的數據,雙向鏈表要比單鏈表佔用更多的內存空間。

鏈表 VS 數組性能大比拼

數組中隨機訪問,指的是按下標的隨機訪問

js實現一個鏈表(知足CRUD)

大體的結構以下:

size和head爲LinkedList構造函數私有屬性,size記錄鏈表中有多少個節點,head指向鏈表的頭結點

單向鏈表的代碼實現

/**
 * 自定義鏈表:對外公開的方法有
 * append(element) 在鏈表最後追加節點
 * insert(index, element) 根據索引index, 在索引位置插入節點
 * remove(element)  刪除節點
 * removeAt(index)  刪除指定索引節點
 * removeAll(element) 刪除全部匹配的節點
 * set(index, element) 根據索引,修改對應索引的節點值
 * get(index)  根據索引獲取節點信息
 * indexOf(element) 獲取某個節點的索引位置
 * clear()  清空全部節點
 * length()   返回節點長度
 * print() 打印全部節點信息
 * toString() 打印全部節點信息,同print
 * */
const LinkedList = function(){
    let head = null;
    let size = 0;   //記錄鏈表元素個數

    //Node模型
    function LinkNode(element, next){
        this.element = element;
        this.next = next;
    }

    //元素越界檢查, 越界拋出異常
    function outOfBounds(index){
        if (index < 0 || index >= size){
            throw("抱歉,目標位置不存在!");
        }
    }

    //根據索引,獲取目標對象
    function node(index){
        outOfBounds(index);

        let obj = head;
        for (let i = 0; i < index; i++){
            obj = obj.next;
        }

        return obj;
    }

    //新增一個元素
     function append(element){
        if (size == 0){
            head = new LinkNode(element, null);
        }
        else{
            let obj = node(size-1);
            obj.next = new LinkNode(element, null);
        }
         size++;
    }

    //插入一個元素
     function insert(index, element){
        if (index == 0){
            head = new LinkNode(element, head);
        }
        else{
            let obj = node(index-1);
            obj.next = new LinkNode(element, obj.next);
        }
         size++;
    }

    //修改元素
    function set(index, element){
        let obj = node(index);
        obj.element = element;
    }

    //根據值移除節點元素
    function remove(element){
        if (size < 1) return null;

        if (head.element == element){
            head = head.next;
            size--;
            return element;
        }
        else{
            let temp = head;
            while(temp.next){
                if (temp.next.element == element){
                    temp.next = temp.next.next;
                    size--;
                    return element;
                }
                else{
                    temp = temp.next;
                }
            }
        }
        return null;
    }

    //根據索引移除節點
     function removeAt(index){
         outOfBounds(index);
         let element = null;

         if (index == 0){
             element = head.element;
             head = head.next;
         }
         else{
             let prev = node(index-1);
             element = prev.next.element;
             prev.next = prev.next.next;
         }
         size--;
        return element;
    }

    //移除鏈表裏面的全部匹配值element的元素
     function removeAll(element){

        let virHead = new LinkNode(null, head); //建立一個虛擬頭結點,head爲次節點
         let tempNode = virHead, ele = null;

         while(tempNode.next){
             if (tempNode.next.element == element){
                 tempNode.next = tempNode.next.next;
                 size--;
                 ele = element;
             }
             else{
                tempNode = tempNode.next;
             }
         }

         //從新賦值
         head = virHead.next;

        return ele;
    }

    //獲取某個元素
    function get(index){
        return node(index).element;
    }

    //獲取元素索引
    function indexOf(element){
        let obj = head, index = -1;

        for (let i = 0; i < size; i++){
            if (obj.element == element){
                index = i;
                break;
            }
            obj = obj.next;
        }
        return index;
    }

    //清除全部元素
    function clear(){
        head = null;
        size = 0;
    }

    //屬性轉字符串
    function getObjString(obj){

        let str = "";

        if (obj instanceof Array){
            str += "[";
            for (let i = 0; i < obj.length; i++){
                str += getObjString(obj[i]);
            }
            str = str.substring(0, str.length - 2);
            str += "], "
        }
        else if (obj instanceof Object){
            str += "{";
            for (var key in obj){
                let item = obj[key];
                str += "\"" + key + "\": " + getObjString(item);
            }
            str = str.substring(0, str.length-2);
            str += "}, "
        }
        else if (typeof obj == "string"){
            str += "\"" + obj + "\"" + ", ";
        }
        else{
            str += obj + ", ";
        }

        return str;
    }
    function toString(){
        let str = "", obj = head;
        for (let i = 0; i < size; i++){
            str += getObjString(obj.element);
            obj = obj.next;
        }
        if (str.length > 0) str = str.substring(0, str.length -2);
        return str;
    }
    //打印全部元素
    function print(){
        console.log(this.toString())
    }

    //對外公開方法
    this.append = append;
    this.insert = insert;
    this.remove = remove;
    this.removeAt = removeAt;
    this.removeAll = removeAll;
    this.set = set;
    this.get = get;
    this.indexOf = indexOf;
    this.length = function(){
        return size;
    }
    this.clear = clear;
    this.print = print;
    this.toString = toString;
}


////測試
// let obj = new LinkedList();
// let obj1 = { title: "全明星比賽", stores: [{name: "張飛vs岳飛", store: "2:3"}, { name: "關羽vs秦瓊", store: "5:5"}]};
//
// obj.append(99);
// obj.append("hello")
// obj.append(true)
// obj.insert(3, obj1);
// obj.insert(0, [12, false, "Good", 81]);
// obj.print();
// console.log("obj1.index: ", obj.indexOf(obj1));
// obj.remove(0);
// obj.removeAll(obj1);
// obj.print();

////測試2
console.log("\n\n......test2.....")
var obj2 = new LinkedList();
obj2.append(8); obj2.insert(1,99); obj2.append('abc'); obj2.append(8); obj2.append(false);
obj2.append(12); obj2.append(8); obj2.append('123'); obj2.append(8);
obj2.print();
obj2.removeAll(8); //刪除全部8
obj2.print();
複製代碼

鏈表的簡單算法(反轉單向鏈表)

/**
 *
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */
 
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let prev = null;
    let curr = head;//定義"哨兵"
    while (curr != null) {
        let nextTemp = curr.next;//暫存剩餘鏈表
        curr.next = prev;//與剩餘鏈表斷開鏈接,並將指向變動
        prev = curr;//兩個節點交互位置
        curr = nextTemp;//指針下移
    }
    return prev;

};
複製代碼

關於,有一個很是貼切的例子,就是一摞疊在一塊兒的盤子。咱們平時放盤子的時候,都是從下往上一個一個放;取的時候,咱們也是從上往下一個一個地依次取,不能從中間任意抽出。後進者先出,先進者後出,這就是典型的「棧」結構

從棧的操做特性上來看

棧是一種「操做受限」的線性表,只容許在一端插入和刪除數據。

實際上,棧既能夠用數組來實現,也能夠用鏈表來實現。用數組實現的棧,咱們叫做順序棧,用鏈表實現的棧,咱們叫做鏈式棧。(這也僅僅針對的是C,C++,JAVA等靜態類型語言)

函數調用棧

操做系統給每一個線程分配了一塊獨立的內存空間,這塊內存被組織成「棧」這種結構, 用來存儲函數調用時的臨時變量。每進入一個函數,就會將臨時變量做爲一個棧幀入棧,當被調用函數執行完成,返回以後,將這個函數對應的棧幀出棧。

function main() {
   let a = 1; 
   let ret = 0;
   let res = 0;
   ret = add(3, 5);
   res = a + ret;
   console.log(`處理結果爲 ${res}`);
   reuturn 0;
}
 
function add( x, y) {
   let sum = 0;
   sum = x + y;
   return sum;
}
複製代碼

圖中顯示的是,在執行到 add() 函數時,函數調用棧的狀況。

棧在表達式求值中的應用

實際上,編譯器就是經過兩個棧來實現的。其中一個保存操做數的棧,另外一個是保存運算符的棧。咱們從左向右遍歷表達式,當遇到數字,咱們就直接壓入操做數棧;當遇到運算符,就與運算符棧的棧頂元素進行比較。

將 3+5*8-6 這個表達式的計算過程畫成了一張圖,以下:

實現瀏覽器的前進、後退功能

使用兩個棧,X 和 Y,咱們把首次瀏覽的頁面依次壓入棧 X,當點擊後退按鈕時,再依次從棧 X 中出棧,並將出棧的數據依次放入棧 Y。當咱們點擊前進按鈕時,咱們依次從棧 Y 中取出數據,放入棧 X 中。當棧 X 中沒有數據時,那就說明沒有頁面能夠繼續後退瀏覽了。當棧 Y 中沒有數據,那就說明沒有頁面能夠點擊前進按鈕瀏覽了。

入棧操做

  1. 好比順序查看了 a,b,c 三個頁面,依次把 a,b,c 壓入棧,這個時候,兩個棧的數據就是這個樣子:
    2.當你經過瀏覽器的後退按鈕,從頁面 c 後退到頁面 a 以後,依次把 c 和 b 從棧 X 中彈出,而且依次放入到棧 Y。這個時候,兩個棧的數據就是這個樣子:

如此反覆如上操做,就能夠簡單解釋瀏覽器回退和前進的操做步驟。

內存中的堆棧vs數據結構堆棧

內存中的堆棧和數據結構堆棧不是一個概念,能夠說內存中的堆棧是真實存在物理區數據結構中的堆棧是抽象的數據存儲結構

內存空間

內存空間在邏輯上分爲三部分:代碼區、靜態數據區和動態數據區,動態數據區又分爲棧區和堆區。

  1. 代碼區:存儲方法體的二進制代碼。高級調度(做業調度)、中級調度(內存調度)、低級調度(進程調度)控制代碼區執行代碼的切換。
  2. 靜態數據區:存儲全局變量、靜態變量、常量,常量包括final修飾的常量和String常量。系統自動分配和回收。
  3. 棧區:存儲運行方法的形參、局部變量、返回值。由系統自動分配和回收。
  4. 堆區:new一個對象的引用或地址存儲在棧區,指向該對象存儲在堆區中的真實數據。

隊列

隊列跟棧同樣,也是一種操做受限的線性表數據結構。

順序隊列和鏈式隊列

隊列跟棧同樣,也是一種抽象的數據結構。它具有先進先出的特性,支持在隊尾插入元素,在隊頭刪除元素。

隊列能夠用數組來實現,也能夠用鏈表來實現。用數組實現的棧叫做順序棧,用鏈表實現的棧叫做鏈式棧。一樣,用數組實現的隊列叫做順序隊列,用鏈表實現的隊列叫做鏈式隊列

代碼實現一個順序隊列

// 用數組實現的隊列
const  ArrayQueue=function(){
  // 數組:items,數組大小:n
  let items =[];
  let n = 0;
  // head 表示隊頭下標,tail 表示隊尾下標
  let head = 0;
  let tail = 0;
 
  // 申請一個大小爲 capacity 的數組
  function createArrayQueue(capacity) {
    items = new Array(capacity);
    n = capacity;
  }
  
  // 入隊操做,將 item 放入隊尾
  function enqueue(item) {
    // tail == n 表示隊列末尾沒有空間了
    if (tail == n) {
      // tail ==n && head==0,表示整個隊列都佔滿了
      if (head == 0) return false;
      // 數據搬移
      for (int i = head; i < tail; ++i) {
        items[i-head] = items[i];
      }
      // 搬移完以後從新更新 head 和 tail
      tail -= head;
      head = 0;
    }
    
    items[tail] = item;
    ++tail;
    return true;
  }
 
  // 出隊
  function dequeue() {
    // 若是 head == tail 表示隊列爲空
    if (head == tail) return null;
    // 爲了讓其餘語言的同窗看的更加明確,把 -- 操做放到單獨一行來寫了
    let ret = items[head];
    ++head;
    return ret;
  }
  
  this.createArrayQueue = createArrayQueue;
  this.enqueue = enqueue;
  this.dequeue = dequeue;
}

複製代碼

循環隊列

用數組實現的隊列,在 tail==n 時,會有數據搬移操做,這樣入隊操做性能就會受到影響。

想要避免數據搬移,能夠用循環隊列來處理。

咱們能夠看到,圖中這個隊列的大小爲 8,當前 head=4,tail=7。當有一個新的元素 a 入隊時,咱們放入下標爲 7 的位置。但這個時候,咱們並不把 tail 更新爲 8,而是將其在環中後移一位,到下標爲 0 的位置。當再有一個元素 b 入隊時,咱們將 b 放入下標爲 0 的位置,而後 tail 加 1 更新爲 1。因此,在 a,b 依次入隊以後,循環隊列中的元素就變成了下面的樣子:

經過這樣的方法,咱們成功避免了數據搬移操做。

代碼實現一個循環隊列

const  CircularQueue = function {
  // 數組:items,數組大小:n
  let items;
  let n = 0;
  // head 表示隊頭下標,tail 表示隊尾下標
  let head = 0;
  let tail = 0;
 
  // 申請一個大小爲 capacity 的數組
  function createCircularQueuee(capacity) {
    items = new Array(capacity);
    n = capacity;
  }
 
  // 入隊
  function enqueue(item) {
    // 隊列滿了
    if ((tail + 1) % n == head) return false;
    items[tail] = item;
    tail = (tail + 1) % n;
    return true;
  }
 
  // 出隊
  function dequeue() {
    // 若是 head == tail 表示隊列爲空
    if (head == tail) return null;
    String ret = items[head];
    head = (head + 1) % n;
    return ret;
  }
  
  this.createCircularQueuee = createCircularQueuee;
  this.enqueue = enqueue;
  this.dequeue = dequeue;
}

複製代碼

未完待續 續集:

  1. 時間複雜度爲O(n²)排序
相關文章
相關標籤/搜索