查找演算法總結(一)—順序、二分、二叉、紅黑

来源:http://www.cnblogs.com/xiangkejin/archive/2017/05/21/6885231.html
-Advertisement-
Play Games

1.順序查找 在查找中我們一個一個順序的遍歷表中的所有鍵並使用equals()方法來查找匹配的鍵。 優點:對數組的結構沒有特定的要求,可以使用數組或者鏈表實現,演算法簡單。 缺點:當數組個數n較大時,效率低下。 時間複雜度:查找命中時,最大時間複雜度是O(n),最小時間複雜度是O(1),平均時間複雜度 ...


1.順序查找

在查找中我們一個一個順序的遍歷表中的所有鍵並使用equals()方法來查找匹配的鍵。

優點:對數組的結構沒有特定的要求,可以使用數組或者鏈表實現,演算法簡單。

缺點:當數組個數n較大時,效率低下。

時間複雜度:查找命中時,最大時間複雜度是O(n),最小時間複雜度是O(1),平均時間複雜度是O(n/2);未命中時,總需要O(n)次比較。

      向一個空表中插入N個不同的件需要N2次比較。

 

2.基於有序數組的二分查找

在查找時,我們先將被查找的鍵和子數組的中間鍵比較。如果被查找的鍵小於中間鍵,我們就在左子數組中繼續查找,如果大於我們就在右子數組中繼續查找,否則中間鍵就是我們要找的鍵。

非遞歸的二分查找:

//非遞歸  
    public int rank(Key key)  
    {  
        int lo = 0, hi = N - 1;  
        while(lo <= hi)  
        {  
            int mid = lo + (hi - lo) / 2;  
            int com = key.compareTo(keys[mid]);  
            if(com < 0)  
            {  
                hi = mid - 1;  
            }  
            else if(com > 0)  
            {  
                lo = mid + 1;  
            }  
            else  
            {  
                return mid;  
            }  
        }  
        return lo;  
    }  
View Code

非遞歸的二分查找:

//遞歸  
    public int rank(Key key,int lo,int hi)  
    {  
        if(hi < lo)  
        {  
            return lo;  
        }  
        int mid = lo + (hi - lo) / 2;  
        int com = key.compareTo(keys[mid]);  
        if(com < 0)  
        {  
            return rank(key,lo,mid-1);  
        }  
        else if(com > 0)  
        {  
            return rank(key,mid + 1,hi);  
        }  
        else  
        {  
            return mid;  
        }  
    }  
      
View Code

一般情況下二分查找都比順序查找快得多,對於一個靜態表(不允許插入)來說,將其在初始化時就排序是值得的。

對於二分查找而言,如果表太大,動態表(插入操作較多)是不適用的。在上表所示,二分查找演算法在插入的時候成本依然是O(n)級別。

核心的問題在於我們能否找到能夠同時保證查找和插入操作都是對數級別的演算法和數據結構。答案是令人興奮的“可以”!

我們如何能夠實現這個目標呢?要支持高效的插入操作,我們似乎需要一種鏈式結構。但單鏈接的鏈表是無法使用二分查找法的,

因為二分查找的高效來自於能夠快速通過索引取得任何子數組的中間元素(但得到一條鏈表的中間元素的唯一方法只能是沿鏈表遍歷)

為了將二分查找的效率和鏈表的靈活性結合起來,我們需要更加複雜的數據結構。能夠同時擁有兩者的就是二叉查找樹

 以上詳細內容可參考博文:淺談演算法和數據結構: 六 符號表及其基本實現

 

3.二叉查找樹

二叉查找樹:是一棵二叉樹,其中每個結點都含有一個鍵以及相關聯的一個值且每個結點的鍵都大於其左子樹中的任意結點的鍵而小於其右子樹中的任意結點的鍵。

二叉查找樹的插入、查找、最大鍵最小鍵、向上取整和向下取整、範圍查找、刪除操作在 演算法(第四版)書中有很好的實現,其中的演算法思想非常值得學習。

可參考博文:淺談演算法和數據結構: 七 二叉查找樹

實現代碼:

public class BTS<Key extends Comparable<Key>,Value> {  
      
    //定義樹  
    public class Node  
    {  
        //左結點  
        private Node left;  
        //右結點  
        private Node right;  
        private Key key;  
        private Value value;  
        private int N;  
          
        public Node(Key key,Value value,int N)  
        {  
            this.key = key;  
            this.value = value;  
            this.N = N;  
        }  
    }  
    //根結點  
    private Node root;  
      
    //獲取樹的大小  
    public int size()  
    {  
        return size(root);  
    }  
    public int size(Node root)  
    {  
        if(root == null)  
        {  
            return 0;  
        }  
        return root.N;  
    }  
      
    //根據鍵獲取相關聯的值  
    public Value get(Key key)  
    {  
        return get(root,key);  
    }  
    public Value get(Node root,Key key)  
    {  
        if(root == null)  
        {  
            return null;  
        }  
        //判斷被查找鍵與當前結點鍵的大小  
        int cmp = root.key.compareTo(key);  
        //若被查找的鍵小於當前結點鍵,則繼續在當前結點的左子樹上查找  
        if(cmp < 0)  
        {  
            return get(root.left,key);  
        }  
        //若被查找的鍵大於當前結點的鍵,則繼續在當前結點的右子樹上查找  
        else if(cmp > 0)  
        {  
            return get(root.right,key);  
        }  
        //若相等,則返回相關聯的值  
        else  
        {  
            return root.value;  
        }  
    }  
      
    //向樹中添加鍵值對,並插入在合適的位置  
    public void put(Key key,Value value)  
    {  
        root = put(root,key,value);  
    }  
    public Node put(Node root,Key key,Value value)  
    {  
        if(root == null)  
        {  
            return new Node(key,value,1);  
        }  
        //插入的鍵與當前結點的鍵進行比較  
        int cmp = root.key.compareTo(key);  
        //若插入的鍵小於當前結點的鍵,則繼續向當前結點的左子樹中插入  
        if(cmp < 0)  
        {  
            root.left = put(root.left,key,value);  
        }  
        //若插入的鍵大於當前結點的鍵,則繼續向當前結點的右子樹中插入  
        else if(cmp > 0)  
        {  
            root.right = put(root.right,key,value);  
        }  
        //若查找到相同的鍵時,則將插入的鍵相關聯的值替換掉當前結點的鍵相關聯的值  
        else  
        {  
            root.value = value;  
        }  
        //將樹的大小加1  
        root.N = size(root.left) + size(root.right) + 1;  
        return root;  
    }  
      
    //獲取樹中最小的鍵  
    public Key min()  
    {  
        return min(root).key;  
    }  
    public Node min(Node x)  
    {  
        //若當前結點的左子樹為空時,則表明當前結點的鍵是樹中最小的鍵,返回該結點  
        if(x.left == null)  
        {  
            return x;  
        }  
        //否則繼續向當前結點的左子樹查找  
        return min(x.left);  
    }  
      
    //對給定鍵進行向下取整  
    //如果給定的鍵小於二叉查找樹的根結點的鍵,  
    //那麼小於等於key的最大鍵floor(key)一定在根結點的左子樹中;  
    //如果給定的鍵大於二叉查找樹的根結點的鍵,  
    //那麼只有當根結點右子樹中存在小於等於key的結點時,  
    //小於等於key的最大鍵才會出現在右子樹中,  
    //否則根結點就是小於等於key的最大鍵。  
    public Key floor(Key key)  
    {  
        Node x = floor(root,key);  
        if(x == null)  
        {  
            return null;  
        }  
        return x.key;  
    }  
    public Node floor(Node root,Key key)  
    {  
        if(root == null)  
        {  
            return null;  
        }  
        int cmp = key.compareTo(root.key);  
        //如果相等,則返回當前結點  
        if(cmp == 0)  
        {  
            return root;  
        }  
        //給定鍵小於當前結點的鍵時,繼續遞歸查找當前結點的左子樹  
        else if(cmp < 0)  
        {  
            return floor(root.left,key);  
        }  
        //給定鍵大於當前結點的鍵時,向當前結點的右子樹繼續遍歷,並將結點返回給t  
        Node t = floor(root.right,key);  
        //若t不為空,則說明右子樹中存在小於等於key的最大鍵,返回t  
        if(t != null)  
        {  
            return t;  
        }  
        //否則返回當前結點  
        else  
        {  
            return root;  
        }  
    }  
      
    //查找排位為key的鍵  
      
      
      
    public Key select(int key)  
    {  
        return select(root,key).key;  
    }  
    public Node select(Node x,int key)  
    {  
        if(x == null)  
        {  
            return null;  
        }  
        int size = size(x.left);  
        //如果key小於左子樹中的結點數size,那麼就繼續遞歸地在左子樹中查找排名為k的鍵  
        if(key < size)  
        {  
            return select(x.left,key);  
        }  
        //如果key大於size,我們就遞歸地在右子樹中查找排名為k-t-1的鍵  
        else if(key > size)  
        {  
            return select(x.right,key - size - 1);  
        }  
        //如果size等於key,返回當前根結點中的鍵  
        else  
        {  
            return x;  
        }  
    }  
      
    //根據給定鍵返回鍵在樹中的排名  
    public int rank(Key key)  
    {  
        return rank(root,key);  
    }  
      
    public int rank(Node x,Key key)  
    {  
        if(x == null)  
        {  
            return 0;  
        }  
        int cmp = key.compareTo(x.key);  
        //如果給定鍵小於當前結點的鍵,則遞歸地向左子樹比較並返回該鍵在左子樹中的排名  
        if(cmp < 0)  
        {  
            return rank(x.left,key);  
        }  
        //如果給定鍵大於當前結點的鍵,則返回左子樹中的結點總數加上1(根結點)再加上右子樹中的排名(遞歸計算)  
        else if(cmp > 0)  
        {  
            return 1 + size(x.left) + rank(x.right,key);  
        }  
        //若相等,則返回當前結點左子樹中的結點總數  
        else  
        {  
            return size(x.left);  
        }  
    }  
      
    //刪除最小鍵  
    public void deleteMin()  
    {  
        deleteMin(root);  
    }  
    public Node deleteMin(Node x)  
    {  
        //若當前結點的左子樹為空,則返回當前結點的右子樹的結點  
        if(x.left == null)  
        {  
            return x.right;  
        }  
        //若當前結點的左子樹不為空,則繼續深入當前結點的左子樹直至遇到空左子樹  
        //進行回溯時將該結點的鏈接指向該結點的右子樹  
        x.left = deleteMin(x.left);  
        //重新計算樹的大小  
        x.N = size(x.left) + size(x.right) + 1;  
        return x;  
    }  
      
    //刪除給定鍵  
      
    public void delete(Key key)  
    {  
        root = delete(root,key);  
    }  
    public Node delete(Node x,Key key)  
    {  
        if(x == null)  
        {  
            return null;  
        }  
        int cmp = key.compareTo(x.key);  
        //若給定鍵小於當前結點的鍵,則繼續深入當前結點的左子樹  
        if(cmp < 0)  
        {  
            x.left = delete(x.left,key);  
        }  
        //若給定鍵大於當前結點的鍵,則繼續深入當前結點的右子樹  
        else if(cmp > 0)  
        {  
            x.right = delete(x.right,key);  
        }  
        //若相等  
        else  
        {  
            //如果當前結點的左子樹為空,則返回當前結點的右子樹結點  
            if(x.left == null)  
            {  
                return x.right;  
            }  
            //如果當前結點的右子樹為空,則返回當前結點的左子樹結點  
            if(x.right == null)  
            {  
                return x.left;  
            }  
            //否則的話:  
            //1.將指向即將被刪除的結點的鏈接保存為t  
            Node t = x;  
            //2.將x指向它的後繼結點min(t.right)  
            x = min(t.right);  
            //3.將x的右鏈接(原本指向一棵所有結點都大於x.key的二叉查找樹)指向deleteMin(t.right),也就是在  
            //刪除後所有的結點仍然都大於x.key的子二叉查找樹  
            x.right = deleteMin(t.right);  
            //4.將x的左鏈接(本為空)設為t.left(其下所有的鍵都小於被刪除的結點和它的後繼結點)  
            x.left = t.left;  
        }  
        //重新計算樹的大小  
        x.N = size(x.left) + size(x.right) + 1;  
        return x;  
    }  
          
} 
View Code

分析:使用二叉查找樹的演算法的運行時間取決於樹的形狀,而樹的形狀又取決於被插入的先後順序,在最好的情況下,一個含有N個節點的樹是完全平衡的,每個空鏈接到根節點的距離都是lgN。

最壞情況下,搜索路徑上可能有N個節點。就這個模型的分析而言,二叉查找樹和快速排序幾乎是”雙胞胎”,樹的根節點就是快速排序中的第一個切分元素。

二叉樹事先的良好性能依賴於其中的鍵的分佈足夠隨機以消除長路徑。對於快速排序,我們可以先將數組打亂,而對於符號表的API,我們無能為力,因為符號表的用例控制著各種操作的先後順序,

。最壞的情況是所有鍵按照順序或者逆序插入符號表。

這個問題依然是可以被解決的,因為還有平衡二叉查找樹,它能保證無論鍵的插入順序如何,樹的高度都將是總鍵數的對數。

 

 4.平衡查找樹

我們希望能保持二叉查找樹的平衡性,所有查找都能在lgN次比較內結束。

4.1 2-3查找樹

為了保證樹的平衡性,我們需要一些靈活性,因此在這裡我們允許樹中的一個節點保存多個鍵。

首先我們引入2-3查找樹,2-3查找樹既有2-節點(一個鍵和兩條鏈接)又有3-節點(兩個鍵和三條鏈接),當然這隻是過渡,過後再引入紅黑樹和B-樹。

和標準的二叉查找樹由上向下生長不同,2-3樹的生長是由下向上的(節點的分解)也正是因為這樣,2-3樹才保證了平衡

具體可參考博文:淺談演算法和數據結構: 八 平衡查找樹之2-3樹

 2-3樹的實現操作是並不方便的,我們需要維護兩種不同類型的節點,將鏈接和其他信息從一個節點複製到另一個節點,將節點從一種數據類型轉換到另一種數據類型等等,

實現這些不僅需要大量的代碼,而且他們所產生的額外開銷可能會使演算法比標準的二叉查找樹更慢。

實際上我們只需要花一點代價就能解決這個問題,就是下麵我們引入的紅黑樹。

 

4.2 紅黑二叉查找樹

紅黑二叉查找樹的基本思想是用標準的二叉查找樹(完全有2-節點構成)和一些額外的信息(替換3-節點)來表示2-3樹。

我們講樹中的鏈接分為兩種類型:左斜的紅色鏈接將兩個2-節點連接起來構成一個3-節點,黑鏈接則是2-3樹中的普通鏈接。

對於任意的2-3樹,只要對節點進行轉換,我們都可以立即派生出一顆對應的二叉查找樹。

等價定義:

   ①紅鏈接均是左鏈接

 ②沒有任何一個節點同時和兩條紅鏈接相連

   ③ 該樹是完美黑色平衡的,即任意空鏈接到根節點的路徑上的黑鏈接數量相同。

通過旋轉和顏色翻轉這些平衡化操作,讓一個紅黑樹保持平衡。紅黑樹並不追求“完全平衡”——它只要求部分地達到平衡要求,降低了對旋轉的要求,從而提高了性能。

具體可參考下麵這篇博文:

淺談演算法和數據結構: 九 平衡查找樹之紅黑樹

 

紅黑樹應用:

 TreeMap 和 TreeSet 是 Java Collection Framework 的兩個重要成員,其中 TreeMap 是 Map 介面的常用實現類,而 TreeSet 是 Set 介面的常用實現類。

雖然 HashMap 和 HashSet 實現的介面規範不同,但 TreeSet 底層是通過 TreeMap 來實現的,因此二者的實現方式完全一樣。而 TreeMap 的實現就是紅黑樹演算法。
 
對於 TreeMap 而言,由於它底層採用一棵“紅黑樹”來保存集合中的 Entry,這意味這 TreeMap 添加元素、取出元素的性能都比 HashMap 低:當 TreeMap 添加元素時,需要通過迴圈找到新增 Entry 的插入位置,因此比較耗性能;當從 TreeMap 中取出元素時,需要通過迴圈才能找到合適的 Entry,也比較耗性能。
 
但 TreeMap、TreeSet 比 HashMap、HashSet 的優勢在於:TreeMap 中的所有 Entry 總是按 key 根據指定排序規則保持有序狀態,TreeSet 中所有元素總是根據指定排序規則保持有序狀態。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 結果: ...
  • 1 Mybatis映射文件--增刪改查 POJO類 EmployeeMapper.xml IEmployeeDAO 配置文件--log4j.properties 配置文件--db.properties 配置文件--mybatis-config.xml 測試類 ...
  • 當客戶端訪問某個能開啟會話功能的資源,web伺服器就會創建一個HTTPSession對象,每個HTTPSession對象都會占用一定的記憶體,如果在同一個時間段內訪問的用戶太多,就會消耗大量的伺服器記憶體,為瞭解決這個問題我們使用一種技術:session的持久化。 什麼是session的持久化? 當客戶 ...
  • 1.request.getContextPath()詳解 <%=request.getContextPath()%>是為瞭解決相對路徑的問題,可返回站點的根路徑。 但不用也可以,比如<a href="<%=request.getContextPath()%>/catalog.jsp">,可以直接用< ...
  • 大家是不是都玩過保齡球?雖然水平很爛,但我是保齡球愛好者。今天這一題是用python來計算保齡球的分數。首先講一下保齡球的規則: 保齡球的一局稱為一個frame,一共有10局。 第1到9局,一般每局可以投擲(roll)兩次,但是有一個例外,就是第一次投擲就全中 - 這種情況稱為strike,打出st ...
  • 、 高級語言運行機制 高級語言按程式的執行方式分為編譯型和解釋型兩種。 java語言比較特殊,Java程式的執行必須經過先編譯後解釋的步驟。 1 編譯生成位元組碼,只面向JVM(.class) 2Jvm執行解釋 JVM:(Java virtual machine) java虛擬機負責解釋執行位元組碼文件 ...
  • 流迭代器 2017-05-21 17:05:51 流迭代器是標準模板庫STL中的,是類模板,流迭代器實例化之後即可以和任何接受對應迭代器的函數一起使用(可以將流看做一個容器,把數據存儲在一個連續的緩衝區中,具有迭代器的功能和類似使用)。 istream_iterator 和ostream_itera ...
  • 因為原文中延續了組合模式的代碼示例來講訪問者模式 所以這裡就合併一起來複習了。但主要還是講訪問者模式。顧名思義這個模式會有一個訪問者類(就像近期的熱播劇“人民的名義”中的檢查官,跑到到貪官家裡調查取證,查實後就定罪),被訪問者類調用訪問者類的時候會將自身傳遞給它使用。直接看代碼: //被訪問者基類 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...