數據結構與算法: 隊列,棧

隊列

顧名思義,理解也很簡單,這個隊列就跟我們排隊上車一樣,先到的排在前面,後到的排在後面,一個一個檢票上車,先到的先上車後到的後上車,其實隊列在生活中還是挺常見,排號相關的都是這個規則,很好理解。kafka,rabbitMQ這些消息隊列,不就是用的隊列的原理

在這裏插入圖片描述

辯證的看問題,如果有先到先出隊的需求,那麼就會有需要後到先出,如地鐵後上車的在門口,到站了就要先出;米缸裏的米,如果快用沒了就補充的話,那麼缸底的米總是用不到的;其實棧在計算機中使用很多的,比如我們經常用得遞歸,其實就是用得棧來存儲函數,遞歸到最後的函數先執行就是這個道理

在這裏插入圖片描述

隊列、棧實例

我們來看一下java中經常用於寫隊列和棧的實例:

棧 Stack,ArrayDeque,LinkedList 比較

在這裏插入圖片描述

Stack,ArrayDeque,LinkedList 這三種都可以用於棧使用,但是性能上有所區分,各有特點:

  • stack 可謂是不叫存正的棧,因爲繼承的是Vector那麼自然是線程安全的,但也繼承了vector的不足性能較差,不過嚴格遵守棧的設計,只能在一頭壓入和彈出不過一般不用這個。
  • ArrayDeque 本質是一個列表,列表前後兩端都可以操作,插入元素不能爲null,因爲是列表無法確定數量時,擴容會影響效率
  • LinkedList 本質是一個雙向鏈表,同樣可以雙向操作,可以插入null,鏈表容量本身不固定 JDK1.6之前官方推薦使用LinkedList集合模擬“棧”結構

總結一下:

  • 需要線程安全: Stack或是使用Collections工具類中synchronizedXxx()將線程不同步的ArrayDeque以及LinkedList轉換成線程同步
  • 頻繁隨機訪問: 由於ArrayDeque是列表,因此這個比較佔優勢
  • 頻繁插入,刪除: LinkedList 鏈表結構,插入刪除時間複雜度爲O(1)
  • 數據量未知: LinkedList

隊列 LinkedList,ArrayBlockingQueue 和PriorityQueue

在這裏插入圖片描述

  • LinkedList 不解釋了,上面已經說過了,因爲可以雙端進出,所以可以用於生成隊列,也是最常見的,線程不安全
  • PriorityQueue 類實質上維護了一個有序列表,隊列的頭指排序規則最小那個元素, 加入隊列的對向會根據規則排序
  • ArrayBlockingQueue 常使用的有界隊列,實質是一個數組
  • ConcurrentLinkedQueue 基於鏈表 線程安全的隊列。增加刪除O(1) 查找O(n)

其中LinkedList, PriorityQueue, ConcurrentLinkedQueue 爲非阻塞隊列,當隊列滿或者隊列空時,只能使用隊列wait(),notify()進行隊列消息傳送

還有一些阻塞隊列實例,就不一一介紹了,想知道爲啥搞了這麼多隊列,可以瞭解一下線程池

隊列、棧相關算法

爲啥隊列和棧要拿出來一起說算法,因爲這兩種數據結構最普遍的應用就是搜索:

  • 廣度優先搜索(BFS)
  • 深度優先搜索 (DFS)

然而一個算法題目能用廣度優先,就能用深度優先,因此基本就是能同時使用。

  • 隊列和廣度優先搜索基本是綁定的
  • 棧和深度優先搜索基本綁定

之前介紹二叉樹的首也有介紹過,各種遍歷方式

感興趣的可以看一下 二叉樹相關算法彙總(詳細)

  • 層序遍歷使用BFS使用隊列
  • 前、中、後序遍歷使用DFS使用棧,當然遞歸也是需要使用到棧,我們使用的是由系統提供的隱式棧,也稱爲調用棧(Call Stack)

總結一下,二叉樹遍歷題目,深度廣度搜索題目,都逃不過這兩種數據結構。

看一下,leetcode上做過的隊列,棧相關題目:

在這裏插入圖片描述

二叉樹佔一半,其實leetcode上題目隊列的題目基本是一些設計類題目,棧的1題目會稍微多一些,根據上面的論調,遞歸也算是棧的題目了😝

廣度優先算法

先看一下leetcode上的廣度優先算法模板:

/**
 * Return the length of the shortest path between root and target node.
 */
int BFS(Node root, Node target) {
    Queue<Node> queue;  // store all nodes which are waiting to be processed
    int step = 0;       // number of steps neeeded from root to current node
    // initialize
    add root to queue;
    // BFS
    while (queue is not empty) {
        step = step + 1;
        // iterate the nodes which are already in the queue
        int size = queue.size();
        for (int i = 0; i < size; ++i) {
            Node cur = the first node in queue;
            return step if cur is target;
            for (Node next : the neighbors of cur) {
                add next to queue;
            }
            remove the first node from queue;
        }
    }
    return -1;          // there is no path from root to target
}

可見廣度優先的算法是通過一層一層的搜索自上而下的順序,最常見的應用也就是,層序遍歷二叉樹:

102. 二叉樹的層序遍歷

    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new ArrayList<>();
        Queue<TreeNode> queue = new ArrayDeque<>();
        if (root != null) queue.add(root);

        while (!queue.isEmpty()) {
            // 獲取本層節點個數
            int size = queue.size();
            List<Integer> level = new ArrayList<>();
            // 通過循環將本層節點全部取出
            for (int i = 0; i < size; ++i) {
                TreeNode node = queue.poll();
                level.add(node.val);
                //添加下層節點
                if (node.left != null) queue.add(node.left);
                if (node.right != null) queue.add(node.right);
            }
            res.add(level);
        }
        return res;
    }

深度優先算法

先看一下leetcode上的深度優先算法模板:

/*
 * Return true if there is a path from cur to target.
 */
boolean DFS(Node cur, Node target, Set<Node> visited) {
    return true if cur is target;
    for (next : each neighbor of cur) {
        if (next is not in visited) {
            add next to visted;
            return true if DFS(next, target, visited) == true;
        }
    }
    return false;
}

這個看似沒有使用到棧,上面也提到過遞歸就是會使用棧,使用的是由系統提供的隱式棧,也稱爲調用棧(Call Stack)。

至於顯式使用stack 最常見的用法可以算是二叉樹前、中、後遍歷的迭代法,舉個例子感受一下:

144. 二叉樹的前序遍歷

    public List<Integer> preorderTraversal(TreeNode root) {
        if (root == null) {
            return new ArrayList<>();
        }
        Stack<TreeNode> stack = new Stack<>();
        List<Integer> res = new ArrayList<>();
        stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            res.add(node.val);
            if (node.right != null) {
                stack.push(node.right);
            }
            if (node.left != null) {
                stack.push(node.left);
            }
        }
        return res;
    }

算法題引用

來看幾道算法題:

200. 島嶼數量,這個是比較典型的DFS/BFS題目:

  • 這個題思路也很簡單就是找到獲取'1'附近的所有'1',劃到一個組,爲'0' return即可
  • 訪問過的就不要在訪問,將訪問了的'1' 改成 '0'

深度優先:

class Solution {
    int maxY, maxX, res=0;
    public int numIslands(char[][] grid) {
        maxY = grid.length;
        if (maxY==0) return res;
        maxX = grid[0].length;

        for (int j=0; j<maxY; ++j) {
            for (int i=0; i<maxX; ++i) {
                if (grid[j][i]=='1') {
                    search(j, i, grid);
                    res++;
                }
            }
        }
        return res;
    }

    public void search(int j, int i, char[][] grid){
        grid[j][i] = '0';
        if (j+1<maxY && grid[j+1][i] == '1') search(j+1,i,grid);
        if (j-1>=0 && grid[j-1][i] == '1') search(j-1,i,grid);
        if (i-1>=0 && grid[j][i-1] == '1') search(j,i-1,grid);
        if (i+1<maxX && grid[j][i+1] == '1') search(j,i+1,grid);
    }
}

廣度優先:

邏輯一樣,就是訪問順序不通而已,就是一個LinkedList,都是先入,一個是後出,一個是先出,遞歸寫起來少一些,邏輯上有點逆向思維的意思

class Solution {
    public int numIslands(char[][] grid) {
        int count = 0;
        for(int i = 0; i < grid.length; i++) {
            for(int j = 0; j < grid[0].length; j++) {
                if(grid[i][j] == '1'){
                    bfs(grid, i, j);
                    count++;
                }
            }
        }
        return count;
    }
    
    private void bfs(char[][] grid, int i, int j){
        Queue<int[]> list = new LinkedList<>();
        list.add(new int[] { i, j });
        while(!list.isEmpty()){
            int[] cur = list.remove();
            i = cur[0]; j = cur[1];
            if(0 <= i && i < grid.length && 0 <= j && j < grid[0].length && grid[i][j] == '1') {
                grid[i][j] = '0';
                list.add(new int[] { i + 1, j });
                list.add(new int[] { i - 1, j });
                list.add(new int[] { i, j + 1 });
                list.add(new int[] { i, j - 1 });
            }
        }
    }
}

再來一題:

面試題13. 機器人的運動範圍

本題注意點:

  • 移動方向選取
  • 邊界問題
  • 訪問記錄

深度優先搜索,使用遞歸:

class Solution {
    int M,N,K,counter=0;
    public int movingCount(int m, int n, int k) {
        M = m;
        N = n;
        K = k;
        dfs(0, 0);
        return counter;
    }

    public void dfs(int i, int j) {
        if ((numSum(i) + numSum(j))<=K) {
            counter ++;
            if((i+1)<M && j%10==0) dfs(i+1,j);
            if((i==0 || (j+1)%10 != 0) && (j+1)<N) dfs(i,j+1);
        }
    }

    public int numSum(int num) {
        int s = 0;
        while (num != 0) {
            s += num % 10;
            num = num / 10;
        }
        return s;
    }
}

廣度優先搜索,使用隊列:

class Solution {
    public int movingCount(int m, int n, int k){
        int counter = 0;
        Queue<int[]> queue= new LinkedList<>();
        queue.add(new int[]{0, 0});

        while (!queue.isEmpty()){
            int[] pos = queue.poll();
            int i = pos[0], j = pos[1];
            if ((numSum(i) + numSum(j))>k) continue;
            counter ++;
            if (i+1<m && j%10==0) queue.add(new int[]{i+1, j});
            if ((i==0 || (j+1)%10 != 0) && (j+1)<n) queue.add(new int[]{i, j+1});
        }
        return counter;
    }

    public int numSum(int num){
        int s = 0;
        while (num != 0){
            s += num % 10;
            num = num / 10;
        }
        return s;
    }
}

總結

對於廣度優先OR深度優先的問題的一些思考:

  • 解題上來說,我自己的經驗是深度優先都會快一些,運行時間短
  • 其實兩種方式遍歷,一個LinkedList,都是先入,一個是後出,一個是先出
  • 至於使用遞歸的情形,逆向思維,不太好想,需要確定好返回點,確定何時結束遞歸,從棧中取值
  • 棧也經常用來做逆序處理
  • 深度優先也有個問題就是如果搜索深度過深,會導致棧容量太大,如果導致內存益處就會報錯,因此搜索深度夠深的話,可以優先考慮廣度優先可控
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章