LeetCode952三部曲之二:小幅度優化(137ms -> 122ms,超39% -> 超51%)

来源:https://www.cnblogs.com/bolingcavalry/archive/2023/09/03/17673519.html
-Advertisement-
Play Games

### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《LeetCode952三部曲》系 ...


歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 本文是《LeetCode952三部曲》系列之二,在前文中,咱們詳細分析瞭解題思路,然後按照思路寫出了代碼,在LeetCode提交成功,成績如下圖所示,137ms,超過39%
    在這裡插入圖片描述
  • 不得不說這個成績很不理想,於是今天咱們來嘗試進行優化,以減低時間,提升百分比

優化點預判

  • 回顧一下題目要求,如下所示
    在這裡插入圖片描述
  • 上圖中有個重要條件:入參數組中,最大值不超過100000
  • 回顧咱們在初始化並查集數據結構的時候,需要滿足數組下標代表數字身份這個特性,例如fathers[100000]=123的含義是數字100000的父節點是123,所以數組長度必須是100001,代碼如下
int[] fathers = new int[100001];
  • 而在實際的並查集操作中,如果入參是4,6,15,35這四個數字,那麼fathers這個數組中真正被我們用到的也就只有fathers[4]、fathers[6]、fathers[15]、fathers[35]這四個元素,其他100001-4=99997個元素都沒有用到,而代碼中還要為這些無用的元素分配空間,還要消耗時間去初始化,這是極大的浪費
  • 對於另一個數組rootSetSize,用於記錄下標位置元素的子樹大小,亦是如此,99997個元素也都浪費了
  • 以上就是要優化的地方:如果入參是四個數字,那麼fathers和rootSetSize的大小能縮減到四嗎?
  • 這就需要分析一下了

優化分析

  • 先回顧一下解題思路,整個流程如下圖所示
    在這裡插入圖片描述

  • 假設此題的入參是這四個數字:4,6,15, 35,回顧什麼時候咱們會用到這四個數字,顯然計算每個數字的質因數的時候必然會用到,計算完成後,得到了下圖的關係(這是前文的內容)
    在這裡插入圖片描述

  • 然後,咱們根據上圖,得到了每個質因數對應的數字集合,也就是下圖
    在這裡插入圖片描述

  • 看著上圖,重點來了:從上圖開始,再到後面的並查集操作,再到最終的結束,都不會用4、6、15、35這樣的數字去計算什麼了

  • 所以,上面那幅圖中的4、6、15、35,是不是可以替換成他們在入參數組中的下標?假設入參數組是[4,6,15,35],他們的數組下標就分別是:0、1、2、3

  • 將數字替換成數組下標後,上面那幅圖的內容就有了變化,變成了下圖的樣子,之前的[4,6,15,35]四個數字變成了[0,1,2,3]
    在這裡插入圖片描述

  • 接下來的並查集操作中,也可以用[0,1,2,3]取代[4,6,15,35]也可以嗎?

  • 當然可以,之前是合併4和6,現在變成了合併0和1,題目是要的是連通的數量,而某個唯一的數字到底是4還是它的數組下標0,這不重要了,重要的是合併不能有錯就行

  • 這樣替換後,如果入參是四個數字,不論值是多少,在並查集操作時,只需要用到它們的數組下標:0、1、2、3,最大也只有3

  • 這就有意思了,數組fathers和rootSetSize的大小從100001變成了入參數組的長度!

  • 準備工作完成了,可以正式動手優化了

優化代碼

  • 首先,要修改的是定義fathers和rootSetSet的代碼,之前是創建固定長度的數組,現在改成先不創建,而是等到後面知道入參數組長度的時候再說
    在這裡插入圖片描述
  • 然後是largestComponentSize方法中的內容,如下圖,存入map的時候,以前存入的是入參的數字,現在傳入的是數字對應的數組下標
    在這裡插入圖片描述
  • 最後還看到一些代碼略有瑕疵,於是順手改了,如下圖,其實影響不大
    在這裡插入圖片描述
  • 以上就是改動的全部了
  • 最後附上優化後的完整源碼
class Solution {
    
    // 並查集的數組, fathers[3]=1的意思是:數字3的父節點是1
//    int[] fathers = new int[100001];
    int[] fathers;

    // 並查集中,每個數字與其子節點的元素數量總和,rootSetSize[5]=10的意思是:數字5與其所有子節點加在一起,一共有10個元素
//    int[] rootSetSize = new int[100001];
    int[] rootSetSize;

    // map的key是質因數,value是以此key作為質因數的數字
    // 例如題目的數組是[4,6,15,35],對應的map就有四個key:2,3,5,7
    // key等於2時,value是[4,6],因為4和6的質因數都有2
    // key等於3時,value是[6,15],因為6和16的質因數都有3
    // key等於5時,value是[15,35],因為15和35的質因數都有5
    // key等於7時,value是[35],因為35的質因數有7
    Map<Integer, List<Integer>> map = new HashMap<>();

    // 用來保存並查集中,最大樹的元素數量
    int maxRootSetSize = 1;

    /**
     * 帶壓縮的並查集查找(即尋找指定數字的根節點)
     * @param i
     */
    private int find(int i) {
        // 如果執向的是自己,那就是根節點了
        if(fathers[i]==i) {
            return i;
        }

        // 用遞歸的方式尋找,並且將整個路徑上所有長輩節點的父節點都改成根節點,
        // 例如1的父節點是2,2的父節點是3,3的父節點是4,4就是根節點,在這次查找後,1的父節點變成了4,2的父節點也變成了4,3的父節點還是4
        fathers[i] = find(fathers[i]);
        return fathers[i];
    }

    /**
     * 並查集合併,合併後,child會成為parent的子節點
     * @param parent
     * @param child
     */
    private void union(int parent, int child) {
        int parentRoot = find(parent);
        int childRoot = find(child);

        // 如果有共同根節點,就提前返回
        if (parentRoot==childRoot) {
            return;
        }

        // child元素根節點是childRoot,現在將childRoot的父節點從它自己改成了parentRoot,
        // 這就相當於child所在的整棵樹都拿給parent的根節點做子樹了
        fathers[childRoot] = fathers[parentRoot];

        // 合併後,這個樹變大了,新增元素的數量等於被合併的字數元素數量
        rootSetSize[parentRoot] += rootSetSize[childRoot];

        // 更像最大數量
        maxRootSetSize = Math.max(maxRootSetSize, rootSetSize[parentRoot]);
    }

    public int largestComponentSize(int[] nums) {

        // 對數組中的每個數,算出所有質因數,構建map
        for (int i=0;i<nums.length;i++) {
            int cur = nums[i];

            for (int j=2;j*j<=cur;j++) {
                // 從2開始逐個增加,能整除的一定是質數
                if(cur%j==0) {
//                    map.computeIfAbsent(j, key -> new ArrayList<>()).add(nums[i]);
                    map.computeIfAbsent(j, key -> new ArrayList<>()).add(i);
                }

                // 從cur中將j的因數全部去掉
                while (cur%j==0) {
                    cur /= j;
                }
            }

            // 能走到這裡,cur一定是個質數,
            // 因為nums[i]被除過多次後結果是cur,所以nums[i]能被cur整除,所以cur是nums[i]的質因數,應該放入map中
            if (cur!=1) {
//                map.computeIfAbsent(cur, key -> new ArrayList<>()).add(nums[i]);
                map.computeIfAbsent(cur, key -> new ArrayList<>()).add(i);
            }
        }

        fathers = new int[nums.length];
        rootSetSize = new int[nums.length];

        // 至此,map已經準備好了,接下來是並查集的事情,先要初始化數組
        for(int i=0;i< fathers.length;i++) {
            // 這就表示:數字i的父節點是自己
            fathers[i] = i;
            // 這就表示:數字i加上其下所有子節點的數量等於1(因為每個節點父節點都是自己,所以每個節點都沒有子節點)
            rootSetSize[i] = 1;
        }

        // 遍歷map
        for (int key : map.keySet()) {
            // 每個key都是一個質因數
            // 每個value都是這個質因數對應的數字
            List<Integer> list = map.get(key);

            int size = list.size();

            // 超過1個元素才有必要合併
            if (size>1) {
                // 取第0個元素作為父節點
                int parent = list.get(0);

                // 將其他節點全部作為地0個元素的子節點
                for(int i=1;i<size;i++) {
                    union(parent, list.get(i));
                }
            }
        }

        return maxRootSetSize;
    }

}
  • 寫完代碼,提交LeetCode,順利AC,咱們將優化前和優化後的數據放在一起對比一下,如下圖,左邊是優化前,右邊是優化後,雖然不能算大幅度提升,但勉強算是有明顯提升了
    在這裡插入圖片描述
  • 至此,第一次優化就完成了,超過50%的成績依舊很一般,還能進一步提升嗎?大幅度提升那種
  • 答案自然是可以,感謝咱們這兩篇的努力,讓我們對解題思路有了深刻理解,接下來,期待第三篇吧,我們會來一次更有效的優化
  • 劇透一下:優化點和算素數有關

歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos

歡迎關註博客園:程式員欣宸

學習路上,你不孤單,欣宸原創一路相伴...


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

-Advertisement-
Play Games
更多相關文章
  • 第一部分為看清:大模型的訓練及推理過程是如何的,以及內部邏輯 第二部分為理解:大模型的訓練及推理和算力的關係 第三部分為推演:用簡單的公式量化大模型算力的需求 第四部分為優化:我們如何提高算力利用率 ...
  • 學習JavaScript的路徑可以按照以下步驟進行: 瞭解基本概念:首先學習JavaScript的基本概念,包括變數、數據類型、運算符、數組、對象、迴圈和條件語句等。可以通過閱讀相關的教材、線上課程或者參考W3Schools和MDN文檔等來學習。 學習控制DOM元素:學習如何使用JavaScript ...
  • 本文希望能夠通過總結過去自己對領域建模的一點粗淺經驗給需要的同學能有些許啟發,少走彎路。 背景 軟體工程師做的核心事情就是對現實世界的問題進行抽象然後用電腦的語言對其進行重新刻畫,在通過信息化來提高生產力。而這其中一個關鍵環節就是如何對問題域進行建模,在過去的工作中經常遇到一個問題是前期因為業務比 ...
  • 分類 懶漢式:實例對象在第一次被使用時才進行初始化。 餓漢式:實例在定義時就被初始化。 特點 1、構造函數和析構函數私有化,不允許外部創建實例對象。 2、拷貝構造函數和複製運算符重載被delete,不允許產生新的實例。 3、內部定義一個私有的靜態數據成員,該成員為本類的實例化對象。 4、提供公有靜態 ...
  • 10分鐘從源碼級別搞懂AQS(AbstractQueuedSynchronizer) ### 前言 上篇文章[15000字、6個代碼案例、5個原理圖讓你徹底搞懂Synchronized](https://juejin.cn/post/7272015112819556412)有說到synchroniz ...
  • 當應用開始脫離單機運行和訪問時,服務發現就誕生了。目前的網路架構是每個主機都有⼀個獨立的 IP 地址,服務發現基本都是通過某種方式獲取到服務所部署的 IP 地址。 DNS 協議是最早將⼀個網路名稱翻譯為網路 IP 的協議,在最初的架構選型中,DNS+LVS+Nginx 基本滿足所有 RESTful ...
  • **條件語句**用於根據不同的條件執行不同的操作。 Go中的條件可以是真或假。 Go支持數學中常見的比較運算符: 小於 大於等於 >= 等於 == 不等於 != 此外,Go還支持常見的邏輯運算符: 邏輯與 && 邏輯或 || 邏輯非 ! 您可以使用這些運算符或它們的組合來創建不同決策的條件。 **示 ...
  • 在實現基於關鍵字的搜索時,首先需要確保MySQL資料庫和ES庫中的數據是同步的。為瞭解決這個問題,可以考慮兩層方案。 1. 全量同步:全量同步是在服務初始化階段將MySQL中的數據與ES庫中的數據進行全量同步。可以在服務啟動時,對ES庫進行全量數據同步操作,以確保數據的一致性。而在停止服務時,可以清 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...