《數(shù)據結構與算法之美》06~10筆記

關于我的倉庫

  • 這篇文章是我為面試準備的學習總結中的一篇
  • 我將準備面試中找到的所有學習資料,寫的Demo,寫的博客都放在了這個倉庫里iOS-Engineer-Interview
  • 歡迎star????
  • 其中的博客在簡書,CSDN都有發(fā)布
  • 博客中提到的相關的代碼Demo可以在倉庫里相應的文件夾里找到

前言

  • 該系列為學習《數(shù)據結構與算法之美》的系列學習筆記
  • 總結規(guī)律為一周一更,內容包括其中的重要知識帶你,以及課后題的解答
  • 算法的學習學與刷題并進,希望能真正養(yǎng)成解算法題的思維
  • LeetCode刷題倉庫:LeetCode-All-In
  • 多說無益,你應該開始打代碼了

06講鏈表(上):如何實現(xiàn)LRU緩存淘汰算法

  • 常見緩存淘汰策略:先進先出策略FIFO(First In,F(xiàn)irst Out)、最少使用策略LFU(Least Frequently Used)、最近最少使用策略LRU(Least Recently Used)。
  • 刪除指定節(jié)點:由于刪除節(jié)點需要使用前一個節(jié)點的next指針,所以對于單鏈表,在執(zhí)行這樣的刪除,插入【前一個插入】的時候,都需要遍歷鏈表
  • 而雙向鏈表對此就很有優(yōu)勢,包括對于有序鏈表查找,由于可以判定往后還是往前【此處存疑,怎么從中間開始?】
  • 對于執(zhí)行較慢的程序,可以通過消耗更多的內存(空間換時間)來進行優(yōu)化;而消耗過多內存的程序,可以 通過消耗更多的時間(時間換空間)來降低內存的消耗。
    BFB09C85-132D-417E-A24F-0D8F707D2F59
  • 在實現(xiàn)上使用的是連續(xù)的內存空間,可以借助CPU的緩存機制,預讀數(shù)組中的數(shù)據,所以訪問效率更高。而 鏈表在內存中并不是連續(xù)存儲,所以對CPU緩存不友好,沒辦法有效預讀。CPU在從內存讀取數(shù)據的時候,會先把讀取到的數(shù)據加載到CPU的緩存中。而CPU每次從內存讀取數(shù)據并不是只讀取那個特定 要訪問的地址,而是讀取一個數(shù)據塊并保存到CPU緩存中,然后下次訪問內存數(shù)據的時候就會先從CPU緩存開始查找,如果找到就不需要再從內存中取。這樣就實現(xiàn)了比內存訪問速度更快的機制,也就是CPU緩存存在的意義:為了彌補內存訪問速度過慢與CPU執(zhí)行速度快之間的差異而引入。對于數(shù)組來說,存儲空間是連續(xù)的,所以在加載某個下標的時候可以把以后的幾個下標元素也加載到CPU緩存這樣執(zhí)行速度會 快于存儲空間不連續(xù)的鏈表存儲。
  • 鏈表本身沒有大小的限制,天然地支持動態(tài)擴容,我覺得這也是它與數(shù)組最大的區(qū) 別。如果我們用ArrayList存儲了了1GB大小的數(shù)據,這個時候已經沒有空閑空間了,當我們再插入數(shù)據 的時候,ArrayList會申請一個1.5GB大小的存儲空間,并且把原來那1GB的數(shù)據拷?到新申請的空間上。聽起來是不是就很耗時?

實現(xiàn)LRU緩存淘汰算法

  • 維護一個單向鏈表,越靠近尾節(jié)點越是遠古且不常用
  • 插入數(shù)據時:
    • 緩存已滿,直接刪除尾節(jié)點,將新數(shù)據插入頭節(jié)點
    • 緩存不滿:
      • 找到的該數(shù)據,刪除原來的,在頭節(jié)點加入
      • 找不到,直接在頭節(jié)點加入

課后題:如何判斷一個字符串是否是回文字符串的問題,我想你應該聽過,我們今天的思題目就是基于這個問題的改造版本。如果字符串是通過單鏈表來存儲的,那該如何來判斷是一個回文串呢?你有什么好的解決思路呢?相應的時間空間復雜度又是多少呢?【LeetCode 234 回文鏈表】

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool isPalindrome(ListNode* head) {
        if (head == NULL || head->next == NULL) {
      return true;
    }

    ListNode *prev = NULL;
    ListNode *slow = head;
    ListNode *fast = head;

    while (fast != NULL && fast->next != NULL) {
      //pre:前一個
      //slow:慢指針&&后一段鏈表的開頭
      //fast:快指針,邊界工具人
      //操作就是要把慢指針經過的逆置過來,所以讓pre作為前一個,slow作為當前工具人,使用next去記錄下一個,就是slow的下一位
      //變換時,先把前面的逆過來,slow再往下走
      fast = fast->next->next;
      ListNode *next = slow->next;
      slow->next = prev;
      prev = slow;
      slow = next;
    }

    if (fast != NULL) {
      //!=NULL的這個情況就是奇數(shù)的情況,此時,slow工具人站在中間,pre就位,因此要讓slow往前一個
      slow = slow->next;
    }

    while (slow != NULL) {
      if (slow->val != prev->val) {
        return false;
      }
      slow = slow->next;
      prev = prev->next;
    }

    return true;
    }
};
  • 先通過快慢指針找到中點,此時鏈表分為兩部分,把前半部分逆置,然后比較兩列

07講鏈表(下):如何輕松寫出正確的鏈表代碼

鏈表六技

技巧一:理解指針或引用的含義

  • 指針的作用就在于存儲對象的內存地址
  • 將某個變量賦值給指針,實際上就是將這個變量的地址賦值給指針,或者反過來說,指針中存儲了這個變量的內存地址,指向了這個變量,通過指針就能找到這個變量。
  • p->next=q:p結點中的next指針存儲了q結點的內存地址
  • p->next=p->next->next:p結點的next指針存儲了p 結點的下下一個結點的內存地址。

技巧二:警惕指針丟失和內存泄漏

  • 這一點其實就是由于鏈表單向性的特征,所以我們需要,我們一旦改變某一個節(jié)點的next指向,就會丟失掉原來那個
  • 插入結點時,一定要注意操作的順序

技巧三:利用哨兵簡化實現(xiàn)難度

  • 針對鏈表的插入,刪除操作,需要對插入第一個節(jié)點和刪除最后一個節(jié)點的情況進行特殊處理
  • 一個使用哨兵的例子:
// 正常人寫的代碼
// 在數(shù)組a中,查找key,返回key所在的位置 // 其中,n表示數(shù)組a的?度
int find(char* a, int n, char key) {
// 邊界條件處理,如果a為空,或者n<=0,說明數(shù)組中沒有數(shù)據,就不用while循環(huán)比較了 
  if(a == null || n <= 0) {
        return -1;
  }
    int i = 0;
// 這里有兩個比較操作:i<n和a[i]==key. 
  while (i < n) {
    if (a[i] == key) {
        return i;
        }
        ++i;
    }
    return -1;
}
// 憨憨哨兵代碼
// 在數(shù)組a中,查找key,返回key所在的位置
// 其中,n表示數(shù)組a的?度
// 我舉2個例子,你可以拿例子走一下代碼 //a={4,2,3,5,9,6} n=6key=7 //a={4,2,3,5,9,6} n=6key=6 
int find(char* a, int n, char key) {
    if(a == null || n <= 0) { 
    return -1;
    }
// 這里因為要將a[n-1]的值替換成key,所以要特殊處理這個值 
  if (a[n-1] == key) {
        return n-1;
  }
// 把a[n-1]的值臨時保存在變量tmp中,以便之后恢復。tmp=6。 
// 之所以這樣做的目的是:希望find()代碼不要改變a數(shù)組中的內容 char tmp = a[n-1];
// 把key的值放到a[n-1]中,此時a = {4, 2, 3, 5, 9, 7} 
  a[n-1] = key;
    int i = 0;
// while 循環(huán)比起代碼一,少了i<n這個比較操作 
  while (a[i] != key) {
        ++i;
  }
// 恢復a[n-1]原來的值,此時a= {4, 2, 3, 5, 9, 6} 
  a[n-1] = tmp;
    if (i == n-1) {
// 如果i == n-1說明,在0...n-2之間都沒有key,所以返回-1 
    return -1;
    } else {
// 否則,返回i,就是等于key值的元素的下標 
    return i;
    } 
}
  • 簡單來說,這么一整就可以使得少了i < n這個判斷,????
  • 當然,這只是為了舉例說明哨兵的作用,你寫代碼的時候千萬不要寫第二段那樣的代碼,因為可讀性太差了。大部分情況下, 我們并不需要如此追求極致的性能。【誰寫誰憨說白了】

技巧四:重點留意邊界條件處理

  • 如果鏈表為空時,代碼是否能正常工作?
  • 如果鏈表僅包含一個結點時,代碼是否能正常工作?
  • 如果鏈表只包含兩個結點時,代碼是否能正常工作?
  • 代碼邏輯在處理頭結點和尾結點的時候,是否能正常工作?

技巧五:舉例畫圖,輔助思考

page7image39834288.jpg

技巧六:多寫多練,沒有捷徑

單鏈表反轉【LeetCode 206 反轉鏈表】

參考文章

題解
我的題解
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        
        ListNode *pre = NULL;
        ListNode *next = NULL;
        ListNode *pNode = head;
        
        while (pNode != NULL) {
            next = pNode->next;
            pNode->next = pre;
            pre = pNode;
            pNode = next;
        }
        
        return pre;
    }
};

參考題解
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        
       if (head == NULL || head->next == NULL) return head;
        ListNode *p = reverseList(head->next);
        head->next->next = head;
        head->next = NULL;
        return p;
    }
};
反思
  • 遞歸騷斷腿,實在難悟

鏈表中環(huán)的檢測【LeetCode 141 環(huán)形鏈表】

參考文章

題解
我的題解
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    bool hasCycle(ListNode *head) {
        if (head == NULL || head->next == NULL) {
            return false;
        }
        ListNode *slow = head;
        ListNode *fast = head;
        while (fast->next != NULL && fast->next->next != NULL) {
            slow = slow->next;
            fast = fast->next->next;
            if (slow->val == fast->val) {
                return true;
            }
        }
        return false;
    }
};

參考題解
public boolean hasCycle(ListNode head) {
    Set<ListNode> nodesSeen = new HashSet<>();
    while (head != null) {
        if (nodesSeen.contains(head)) {
            return true;
        } else {
            nodesSeen.add(head);
        }
        head = head.next;
    }
    return false;
}
反思
  • 要想鏈表玩的溜,兩根指針少不了

兩個有序的鏈表合并【LeetCode 21 合并兩個有序鏈表】

參考文章

題解
我的題解
//這誰會呢

參考題解
//遞歸
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 == NULL) {
            //這一步不僅是一開始的判空,更是后面當一個鏈表已經遍歷到結束了
            //就一直使用另一個里面的節(jié)點
            return l2;
        } 
        else if (l2 == NULL) {
            return l1;
        }
        else if (l1->val < l2->val) {
            //由于從小到大,因此
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        }
        else {
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
    }
};

//迭代
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        //這等于就是添加哨兵的思想,使得不用考慮開頭的數(shù)字,妙啊
        ListNode *prehead = new ListNode(-1);

      
        ListNode *prev = prehead;
        while (l1 != NULL && l2 != NULL) {
            if (l1->val <= l2->val) {
                //把l1連上
                prev->next = l1;
                //接下來要做的僅僅是把l1指針指向下一個
                l1 = l1->next;
            } else {
                prev->next = l2;
                l2 = l2->next;
            }
            prev = prev->next;
        }
        prev->next = l1 == NULL ? l2 :l1;

        return prehead->next;
    }
};
反思
  • 還是想太復雜了,其實只要進行一下鏈接就行,不用考慮辣么多

刪除鏈表倒數(shù)第n個結點【LeetCode 19 刪除鏈表的倒數(shù)第N個節(jié)點】

參考文章
題解
我的題解
//這誰會呢

參考題解
//兩次遍歷算法
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *dummy = new ListNode(0);
        dummy->next = head;
        int length  = 0;
        ListNode *first = head;
        while (first != NULL) {
            length++;
            first = first->next;
        }
        length -= n;
        first = dummy;
        while (length > 0) {
            length--;
            first = first->next;
        }
        first->next = first->next->next;
        return dummy->next;
    }
};

//一次遍歷法
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode *dummy = new ListNode(0);
        dummy->next = head;
        ListNode *first = dummy;
        ListNode *second = dummy;
        for (int i = 1; i <= n + 1; i++) {
            first = first->next;
        }
        while (first != NULL) {
            first = first->next;
            second = second->next;
        }
        second->next = second->next->next;
        return dummy->next;
    }
};
反思
  • 這又啥難的,驚了

求鏈表的中間結點【LeetCode 876 鏈表的中間結點】

參考文章
題解
我的題解
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* middleNode(ListNode* head) {
        ListNode *slowNode = head;
        ListNode *fastNode = head;
        while (fastNode->next != NULL && fastNode->next->next != NULL) {
            fastNode = fastNode->next->next;
            slowNode = slowNode->next;
        }
        if (fastNode->next != NULL) {
            slowNode = slowNode->next;
        }
        return slowNode;
    }
};

參考題解
//這要啥參考題解呢
反思
  • 聯(lián)動題目,回文鏈表

課后題:今天我們講到用哨兵來簡化編碼實現(xiàn),你是否還能夠想到其他場景,利用哨兵可以大大地簡化編碼難度?

  • 舉例:iOS開發(fā)中,對于autoreleasing Pool那一塊,在每個autoreleasingPool之間是靠哨兵對象nil隔開的
  • 具體看我這篇博客從RunTime源碼回看autoreleasepool

08講棧:如何實現(xiàn)瀏覽器的前進和后退功能

  • 后進者先出,先進者后出,這就是典型的結構
  • 當某個數(shù)據集合只涉及在一端插入和刪除數(shù)據,并且滿足后進先出、先進后出的特性,我們就應該首選這種數(shù)據結構。

順序棧的基本實現(xiàn)

EF09A73A-1691-4D2F-9818-FEBADC0B04A9
  • 存儲數(shù)據需要一個長度為n的數(shù)組并不是說空間復雜度就是O(n),因為這n個空間是必須的,空間復雜度應該是算法所需要的額外存儲空間

支持動態(tài)擴容的順序棧

page4image7848160.png
  • 最好情況時間復雜度是O(1),最壞情況時間復雜度是O(n),使用攤還分析法分析
page5image7775648.png
  • 總共涉及了K個數(shù)據的搬移,以及K次simple-push操作。將K個數(shù)據搬移均攤到K次入棧 操作,那每個入棧操作只需要一個數(shù)據搬移和一個simple-push操作。以此類推,入棧操作的均攤時間復雜度就為O(1)。
  • 因為在大部分情況下,入 棧操作的時間復雜度O都是O(1),只有在個別時刻才會退化為O(n),所以把耗時多的入棧操作的時間均攤到其他入棧操作上, 平均情況下的耗時就接近O(1)。

典型使用

棧在函數(shù)調用中的應用【函數(shù)調用棧】

  • 作系統(tǒng)給每個線程分配了一塊獨立的內存空間,這塊內存被組織成“棧”這種結構,用來存儲函數(shù)調用時的臨時變 量。每進入一個函數(shù),就會將臨時變量作為一個棧幀入棧,當被調用函數(shù)執(zhí)行完成,返回之后,將這個函數(shù)對應的棧幀出棧。
  • 【程序員的自我修養(yǎng)p.287】:每一次函數(shù)的調用,都會在調用棧(call stack)上維護一個獨立的棧幀(stack frame).每個獨立的棧幀一般包括函數(shù)的返回地址和參數(shù), 臨時變量: 包括函數(shù)的非靜態(tài)局部變量以及編譯器自動生成的其他臨時變量, 保存的上下文【包括在函數(shù)調用前后需要不變的寄存器】
//
//  main.cpp
//  stack-Test
//
//  Created by Kevin.J on 2019/9/11.
//  Copyright ? 2019 姜凱文. All rights reserved.
//

#include <iostream>

int add(int x, int y) {
    int sum = 0;
    sum = x + y;
    return sum;
}

int main(int argc, const char * argv[]) {
    // insert code here...
    std::cout << "Hello, World!\n";
    
    int a = 1;
    int ret = 0;
    int res = 0;
    ret = add(3, 5);
    res = a + ret;
    printf("%d", res);
    return 0;
}
  • 我們在add中打上一個斷電,通過Xcode自帶的LLDB來查看下棧幀
//輸入 thread backtrace
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
  * frame #0: 0x00000001000011d1 stack-Test`add(x=3, y=5) at main.cpp:13:11
    frame #1: 0x0000000100001234 stack-Test`main(argc=1, argv=0x00007ffeefbff5f0) at main.cpp:24:11
    frame #2: 0x00007fff6bf9f3d5 libdyld.dylib`start + 1
//當前頂上的就是add
page6image7649056.png

棧在表達式求值中的應用

  • 編譯器就是通過兩個棧來實現(xiàn)的。其中一個保存操作數(shù)的棧,另一個是保存運算符的棧。我們從左向右遍歷表達式, 當遇到數(shù)字,我們就直接壓入操作數(shù)棧;當遇到運算符,就與運算符棧的棧頂元素進行比較。
page7image8074704.png
  • 這里還是要借助下西郵的數(shù)據結構課來說明比較妥當表達式求值問題
  • 數(shù)據優(yōu)先級:
0301B223-3494-44BE-B268-394991C9125E
  • 這里的圖畫的不清楚,實際上在計算完8 * 5后得到40,會把40這個數(shù)重新壓入數(shù)據棧,接下來也不會急著讀取下一位,而是繼續(xù)比較,發(fā)現(xiàn)減號優(yōu)先級大于棧頂?shù)募犹枺蔷驮谔幚砑臃ǎ@時在比較遇到的是#,優(yōu)先級比減號大了,此時才會把減號壓入

棧在括號匹配中的應用

  • 這個沒什么好說的,就是遇到做括號入棧,遇到右括號出棧

通過棧實現(xiàn)瀏覽器的前進和后退

  • 我們使用兩個棧,X和Y,我們把首次瀏覽的?面依次壓入棧X,當點擊后退按鈕時,再依次從棧X中出棧,并將出棧的數(shù)據依 次放入棧Y。當我們點擊前進按鈕時,我們依次從棧Y中取出數(shù)據,放入棧X中。當棧X中沒有數(shù)據時,那就說明沒有?面可以 繼續(xù)后退瀏覽了。當棧Y中沒有數(shù)據,那就說明沒有?面可以點擊前進按鈕瀏覽了。
page8image7821888.png
  • 當你通過瀏覽器的后退按鈕,從?面c后退到?面a之后,我們就依次把c和b從棧X中彈出,并且依次放入到棧Y。這個時候, 兩個棧的數(shù)據就是這個樣子:
page8image7823680.png
  • 這個時候你又想看?面b,于是你又點擊前進按鈕回到b?面,我們就把b再從棧Y中出棧,放入棧X中。此時兩個棧的數(shù)據是這 個樣子:
page9image7397920.png

課后題

我們在講棧的應用時,講到用函數(shù)調用棧來保存臨時變量,為什么函數(shù)調用要用“棧”來保存臨時變量呢?用其他數(shù)據結構 不行嗎?

  • 其實,我們不一定非要用棧來保存臨時變量,只不過如果這個函數(shù)調用符合后進先出的特性,用棧這種數(shù)據結構來實現(xiàn),是最順理成章的選擇。

    從調用函數(shù)進入被調用函數(shù),對于數(shù)據來說,變化的是什么呢?是作用域。所以根本上,只要能保證每進入一個新的函數(shù),都是一個新的作用域就可以。而要實現(xiàn)這個,用棧就非常方便。在進入被調用函數(shù)的時候,分配一段棧空間給這個函數(shù)的變量,在函數(shù)結束的時候,將棧頂復位,正好回到調用函數(shù)的作用域內。

我們都知道,JVM內存管理中有個“堆棧”的概念。棧內存用來存儲局部變量和方法調用,堆內存用來存儲Java中的對象。 那JVM里面的“棧”跟我們這里說的“棧”是不是一回事呢?如果不是,那它為什么又叫作“棧”呢?

  • JVM里面的棧和我們這里說的是一回事,被稱為方法棧。和前面函數(shù)調用的作用是一致的,用來存儲方法中的局部變量。

作業(yè)

  • leetcode上關于棧的題目大家可以先做20,155,232,844,224,682,496
  • 我就不貼過來了,在我的LeetCode倉庫里有

09講隊列:隊列在線程池等有限資源池中的應用

  • 這一節(jié)沒啥新東西,完全是數(shù)據結構課上的知識,咱就總結總結過去

如何理解“隊列”?

  • 隊列跟棧非常相似,支持的操作也很有限,最基本的操作也是兩個:入隊enqueue(),放一個數(shù)據到隊列尾部;出隊dequeue(),從隊列頭部取一個元素。
img
  • 隊列的應用也非常廣泛,特別是一些具有某些額外特性的隊列,比如循環(huán)隊列、阻塞隊列、并發(fā)隊列。它們在很多偏底層系統(tǒng)、框架、中間件的開發(fā)中,起著關鍵性的作用。比如高性能隊列Disruptor、Linux環(huán)形緩存,都用到了循環(huán)并發(fā)隊列;Java concurrent并發(fā)包利用ArrayBlockingQueue來實現(xiàn)公平鎖等。

順序隊列和鏈式隊列

  • 用數(shù)組實現(xiàn)的隊列叫作順序隊列,用鏈表實現(xiàn)的隊列叫作鏈式隊列

順序隊列

// 順序隊列的實現(xiàn)
// 用數(shù)組實現(xiàn)的隊列
public class ArrayQueue {
  // 數(shù)組:items,數(shù)組大小:n
  private String[] items;
  private int n = 0;
  // head表示隊頭下標,tail表示隊尾下標
  private int head = 0;
  private int tail = 0;

  // 申請一個大小為capacity的數(shù)組
  public ArrayQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入隊
  public boolean enqueue(String item) {
    // 如果tail == n 表示隊列已經滿了
    if (tail == n) return false;
    items[tail] = item;
    ++tail;
    return true;
  }

  // 出隊
  public String dequeue() {
    // 如果head == tail 表示隊列為空
    if (head == tail) return null;
    // 為了讓其他語言的同學看的更加明確,把--操作放到單獨一行來寫了
    String ret = items[head];
    ++head;
    return ret;
  }
}
  • 當a、b、c、d依次入隊之后,隊列中的head指針指向下標為0的位置,tail指針指向下標為4的位置。
img
  • 當我們調用兩次出隊操作之后,隊列中head指針指向下標為2的位置,tail指針仍然指向下標為4的位置。
img
  • 解決順序隊列浪費空間的缺點:數(shù)據搬移【改寫入隊函數(shù)】
 // 入隊操作,將item放入隊尾
  public boolean enqueue(String item) {
    // tail == n表示隊列末尾沒有空間了
    if (tail == n) {
      // tail ==n && head==0,表示整個隊列都占滿了
      if (head == 0) return false;
      // 數(shù)據搬移
      for (int i = head; i < tail; ++i) {
        items[i-head] = items[i];
      }
      // 搬移完之后重新更新head和tail
      tail -= head;
      head = 0;
    }
    
    items[tail] = item;
    ++tail;
    return true;
  }
  • 從代碼中我們看到,當隊列的tail指針移動到數(shù)組的最右邊后,如果有新的數(shù)據入隊,我們可以將head到tail之間的數(shù)據,整體搬移到數(shù)組中0到tail-head的位置。
img
  • 這里入隊的時間復雜度,使用均攤計算法,肯定還是O(1)

鏈式隊列

img

循環(huán)隊列

  • 循環(huán)隊列,顧名思義,它長得像一個環(huán)。原本數(shù)組是有頭有尾的,是一條直線。現(xiàn)在我們把首尾相連,扳成了一個環(huán)。我畫了一張圖,你可以直觀地感受一下。
img
img
  • 隊列空的條件還是head = tail,滿的條件是(tail+1)%n=head
img
  • tail=3,head=4,n=8,所以總結一下規(guī)律就是:(3+1)%8=4
  • 此時tail不存放數(shù)據,浪費一個空間
public class CircularQueue {
  // 數(shù)組:items,數(shù)組大小:n
  private String[] items;
  private int n = 0;
  // head表示隊頭下標,tail表示隊尾下標
  private int head = 0;
  private int tail = 0;

  // 申請一個大小為capacity的數(shù)組
  public CircularQueue(int capacity) {
    items = new String[capacity];
    n = capacity;
  }

  // 入隊
  public boolean enqueue(String item) {
    // 隊列滿了
    if ((tail + 1) % n == head) return false;
    items[tail] = item;
    tail = (tail + 1) % n;
    return true;
  }

  // 出隊
  public String dequeue() {
    // 如果head == tail 表示隊列為空
    if (head == tail) return null;
    String ret = items[head];
    head = (head + 1) % n;
    return ret;
  }
}

阻塞隊列和并發(fā)隊列

  • 阻塞隊列其實就是在隊列基礎上增加了阻塞操作。簡單來說,就是在隊列為空的時候,從隊頭取數(shù)據會被阻塞。因為此時還沒有數(shù)據可取,直到隊列中有了數(shù)據才能返回;如果隊列已經滿了,那么插入數(shù)據的操作就會被阻塞,直到隊列中有空閑位置后再插入數(shù)據,然后再返回。
img
  • 這就涉及到“生產者-消費者模型”【在實際的軟件開發(fā)過程中,經常會碰到如下場景:某個模塊負責產生數(shù)據,這些數(shù)據由另一個模塊來負責處理(此處的模塊是廣義的,可以是類、函數(shù)、線程、進程等)。產生數(shù)據的模塊,就形象地稱為生產者;而處理數(shù)據的模塊,就稱為消費者】
  • 這種基于阻塞隊列實現(xiàn)的“生產者-消費者模型”,可以有效地協(xié)調生產和消費的速度。當“生產者”生產數(shù)據的速度過快,“消費者”來不及消費時,存儲數(shù)據的隊列很快就會滿了。這個時候,生產者就阻塞等待,直到“消費者”消費了數(shù)據,“生產者”才會被喚醒繼續(xù)“生產”。
img
  • 線程安全的隊列我們叫作并發(fā)隊列。最簡單直接的實現(xiàn)方式是直接在enqueue()、dequeue()方法上加鎖,但是鎖粒度大并發(fā)度會比較低,同一時刻僅允許一個存或者取操作。實際上,基于數(shù)組的循環(huán)隊列,利用CAS原子操作,可以實現(xiàn)非常高效的并發(fā)隊列。這也是循環(huán)隊列比鏈式隊列應用更加廣泛的原因。

線程池沒有空閑線程時,新的任務請求線程資源時,線程池該如何處理?各種處理策略又是如何實現(xiàn)的呢?

  • 我們一般有兩種處理策略。第一種是非阻塞的處理方式,直接拒絕任務請求;另一種是阻塞的處理方式,將請求排隊,等到有空閑線程時,取出排隊的請求繼續(xù)處理。
  • 基于鏈表的實現(xiàn)方式,可以實現(xiàn)一個支持無限排隊的無界隊列(unbounded queue),但是可能會導致過多的請求排隊等待,請求處理的響應時間過長。所以,針對響應時間比較敏感的系統(tǒng),基于鏈表實現(xiàn)的無限排隊的線程池是不合適的。
  • 而基于數(shù)組實現(xiàn)的有界隊列(bounded queue),隊列的大小有限,所以線程池中排隊的請求超過隊列大小時,接下來的請求就會被拒絕,這種方式對響應時間敏感的系統(tǒng)來說,就相對更加合理。不過,設置一個合理的隊列大小,也是非常有講究的。隊列太大導致等待的請求太多,隊列太小會導致無法充分利用系統(tǒng)資源、發(fā)揮最大性能。
  • 實際上,對于大部分資源有限的場景,當沒有空閑資源時,基本上都可以通過“隊列”這種數(shù)據結構來實現(xiàn)請求排隊。

課后題

除了線程池這種池結構會用到隊列排隊請求,你還知道有哪些類似的池結構或者場景中會用到隊列的排隊請求呢?

  • iOS:GCD就是通過隊列的應用進行的線程管理
  • 分布式應用中的消息隊列,也是一種隊列結構

今天講到并發(fā)隊列,關于如何實現(xiàn)無鎖并發(fā)隊列,網上有非常多的討論。對這個問題,你怎么看呢?

  • 考慮使用CAS實現(xiàn)無鎖隊列,則在入隊前,獲取tail位置,入隊時比較tail是否發(fā)生變化,如果否,則允許入隊,反之,本次入隊失敗。出隊則是獲取head位置,進行cas。

作業(yè):循環(huán)隊列【LeetCode 622. 設計循環(huán)隊列】

  • 我感覺我這代碼沒有任何問題呀,就是過不了
class MyCircularQueue {
public:
    /** Initialize your data structure here. Set the size of the queue to be k. */
    MyCircularQueue(int k) {
        size = k;
        head = 0;
        tail = 0;
        data.resize(k);
    }
    
    /** Insert an element into the circular queue. Return true if the operation is successful. */
    bool enQueue(int value) {
        if ((tail + 1) % size == head) {
            return false;
        }
        data[tail] = value;
        tail = (tail + 1) % size;
        return true;
    }
    
    /** Delete an element from the circular queue. Return true if the operation is successful. */
    bool deQueue() {
        if (head == tail) {
            return false;    
        }
        head = (head + 1) % size;
        return true;
    }
    
    /** Get the front item from the queue. */
    int Front() {
        if (head == tail) {
            return -1;    
        }
        return data[head];
    }
    
    /** Get the last item from the queue. */
    int Rear() {
        if (head == tail) {
            return -1;    
        }
        return data[(tail - 1 + size) % size];
    }
    
    /** Checks whether the circular queue is empty or not. */
    bool isEmpty() {
        if (head == tail) {
            return true;    
        }
        return false;
    }
    
    /** Checks whether the circular queue is full or not. */
    bool isFull() {
        if ((tail + 1) % size == head) {
            return true;
        }
        return false;
    }
    
private:
    vector<int> data;
    int size, head, tail;
};

/**
 * Your MyCircularQueue object will be instantiated and called as such:
 * MyCircularQueue* obj = new MyCircularQueue(k);
 * bool param_1 = obj->enQueue(value);
 * bool param_2 = obj->deQueue();
 * int param_3 = obj->Front();
 * int param_4 = obj->Rear();
 * bool param_5 = obj->isEmpty();
 * bool param_6 = obj->isFull();
 */

10講遞歸:如何用三行代碼找到“最終推薦人”

如何理解“遞歸”?

  • 去的過程要將遞,來的過程叫歸
  • 關鍵在與找出遞歸推導式

遞歸需要滿足的三個條件

  • 一個問題的解可以分解為幾個子問題的解
  • 這個問題與分解之后的子問題,除了數(shù)據規(guī)模不同,求解思路完全一樣
  • 存在遞歸終止條件
  • 這里我們用一個Leetcode上的簡單遞歸題來總結下:

Leetcode 687. 最長同值路徑

給定一個二叉樹,找到最長的路徑,這個路徑中的每個節(jié)點具有相同值。 這條路徑可以經過也可以不經過根節(jié)點。

注意:兩個節(jié)點之間的路徑長度由它們之間的邊數(shù)表示。

示例 1:

輸入:

          5
         / \
        4   5
       / \   \
      1   1   5

輸出:

2
示例 2:

輸入:

          1
         / \
        4   5
       / \   \
      4   4   5

輸出:

2
注意: 給定的二叉樹不超過10000個結點。 樹的高度不超過1000。

分析:

  • 一個問題的解可以分解為幾個子問題的解
    • 首先對于樹形結構,我們肯定是從根結點往下遍歷,對于每個結點,我們的長度本質是對其在左結點以及右結點路徑和上在+1
    • 當然還是很難懂,怪不得說遞歸是最難的了,蝦米玩意
  • 這個問題與分解之后的子問題,除了數(shù)據規(guī)模不同,求解思路完全一樣
    • 這個在第一點也說明的很清楚了
  • 存在遞歸終止條件
    • 這個很簡單,對與樹形結構,在碰到結點為NULL肯定終止了
  • 好難,感覺自己做還是整不出來
  • 只能理解下

代碼:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    int ans;
    int longestUnivaluePath(TreeNode* root) {
        ans = 0;
        arrowLength(root);
        return ans;
    }
    int arrowLength(TreeNode* node) {
        if (node == NULL) {
            return 0;
        }
        int left = arrowLength(node->left);
        int right = arrowLength(node->right);
        int arrowLeft = 0, arrowRight = 0;
        if (node->left != NULL && node->left->val == node->val) {
            arrowLeft += left + 1;
        }
        if (node->right != NULL && node->right->val == node->val) {
            arrowRight += right + 1;
        }
        ans = max(ans, arrowLeft + arrowRight);
        return max(arrowLeft, arrowRight);
    }
};

遞歸心法

  • 寫遞歸代碼的關鍵就是找到如何將大問題分解為小問題的規(guī)律,并且基于此寫出遞推公式,然后再推敲終止條件,最后將遞推公式和終止條件翻譯成代碼
  • 下面這一點我覺得說的很有道理,對于遞歸,關鍵在于不要糾結,想著吧所有代碼鋪開去思考整個過程,比如說對于上面這道題,我們在計算f(n)的時候,就假設n-1結果已經出來,直接用就完了,只思考一次的調用去寫我們的遞歸代碼,可能是個比較好的習慣
  • 編寫遞歸代碼的關鍵是,只要遇到遞歸,我們就把它抽象成一個遞推公式,不用想一層層的調用關系,不要試圖用人腦去分解遞歸的每個步驟

警惕棧溢出

  • 這一段在講了函數(shù)調用棧之后應該已經很好理解了,其實就是說由于在遞歸時,我們時函數(shù)套函數(shù),導致函數(shù)調用棧被占用很多內存,會出現(xiàn)溢出風險
  • 這里通常做法是設置一下遞歸深度最大值

遞歸代碼警惕重復計算

img
  • 我們看到為了計算一個f(6),我們計算了好幾次f(3),這就是重復計算
  • 解決方法一般是使用一個哈希表來存儲值,下次用到的時候直接查詢使用就行
  • 另外由于遞歸調用一次就會在內存棧中保存一次現(xiàn)場數(shù)據所以空間復雜度基本上不會是O(1)

迭代法與遞歸相互轉化

  • 基本上所有遞歸代碼都可以轉化,由于遞歸的本質上是使用棧來實現(xiàn)的,只是他的使用是依靠系統(tǒng)做的,我們無法感知到
  • 所以可以使用迭代法,手動實現(xiàn)出入棧

開篇解答:如何找到“最終推薦人”?

  • 給定一個用戶ID,如何查找這個用戶的“最終推薦人”?
img
long findRootReferrerId(long actorId) {
  Long referrerId = select referrer_id from [table] where actor_id = actorId;
  if (referrerId == null) return actorId;
  return findRootReferrerId(referrerId);
}
  • 上述代碼的漏洞:
    • 如果遞歸很深,可能會有堆棧溢出的問題
    • 如果數(shù)據庫里存在臟數(shù)據,我們還需要處理由此產生的無限遞歸問題。比如demo環(huán)境下數(shù)據庫中,測試工程師為了方便測試,會人為地插入一些數(shù)據,就會出現(xiàn)臟數(shù)據。如果A的推薦人是B,B的推薦人是C,C的推薦人是A,這樣就會發(fā)生死循環(huán)

課后題:我們平時調試代碼喜歡使用IDE的單步跟蹤功能,像規(guī)模比較大、遞歸層次很深的遞歸代碼,幾乎無法使用這種調試方式。對于遞歸代碼,你有什么好的調試方法呢?

  • 打印日志發(fā)現(xiàn),遞歸值;結合條件斷點進行調試
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內容