LeetCode952三部曲之三:再次優化(122ms -> 96ms,超51% -> 超91%)

来源:https://www.cnblogs.com/bolingcavalry/archive/2023/09/04/17673524.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三部曲之三》的終篇,先回顧一下前文的成果,看看我們之前已經優化到什麼程度:
    在這裡插入圖片描述
  • 前文的優化思路是減小並查集數組的規模,帶來的結果是節省記憶體、減少數組相關的執行次數,但從代碼上分析,並查集數組處理所占比重並不多,所以造成此處整體優化效果一般
  • 所以,除了並查集,還要去尋找其他優化點,這就是本篇的主要內容

優化思路

  • 尋找優化點的方向很明確:重點關註時間複雜度高的代碼塊
  • 按照上述思路,很容易就找到了下圖中的代碼段,位於程式入口位置,計算每個數字的質因數,因為涉及到素數,所以時間複雜度較高,三個耗時操作是嵌套關係
    在這裡插入圖片描述
  • 上述方法的思路對每個數字做計算,找出質因數,例如找出99的質因數,需要從2開始一次次計算得出
  • 但實際上還有一個更簡單的思路:99以內的質數是固定的25個,這25個中,其平方小於99的只有四個,既:2,3,5,7,所以尋找99的質即因數,就在這四個中找即可(漏掉的11,在後面的代碼中會特別處理找回來)
  • 基於以上思路,計算質因數的代碼就很簡單了:
  1. 提前把100000以內的所有素數都找出來,放在名為primes的數組中
  2. 對於任意一個數字N,都用primes中的數字去做除法,能整除的就是N的質因數
  3. 記得像前面的99漏掉了11那樣,把11找回來

編碼

  • 接下來的代碼,在前文的基礎上修改
  • 首先增加三個靜態變數,註釋已詳細說明其作用:
    // isPrime[3]=0,表示數字3是素數,isPrime[4]=1,表示數字4不是素數
    private static int[] isPrime = new int[100001];

    // 0-100001之間所有的素數都放入這裡
    private static int[] primes = new int[100001];

    // 素數的數量,也就是primes中有效數據的長度
    private static int primeNum = 0;
  • 然後是一個靜態代碼塊,一次性算處100000範圍內所有素數,埃式或者歐拉式都可以,這裡用了歐拉式
static {

        // 歐拉篩
        for(int i=2;i<=100000;i++) {
            if(isPrime[i]==0) {
                // i是素數,就放入primes數組中
                // 更新primes中素數的數量
                primes[primeNum++] = i;
            }

            for(int j=0;i*primes[j]<=100000;j++) {
                // primes[j]*i的結果是個乘積,這樣的數字顯然不是素數,所以在isPrimes數組中標註為1
                isPrime[primes[j]*i] = 1;

                // 如果i是primes中某個素數的倍數,就沒有必要再計算了,退出算下一個,
                // 例如i=8的時候,其實在之前i=4時就已經計算出8不是素數了
                if(i%primes[j]==0) {
                    break;
                }
            }
        }

        // 經過以上代碼,0-100001之間所有素數都放入了primes中
    }
  • 上述代碼只會在類載入後執行一次,執行完畢後,1到100000之間的所有素數都計算出來並放入primes中,數量是primeNum,在後面的計算中直接拿來用即可
  • 接下來是最關鍵的地方了,前面截圖中對每個數字計算質因數的代碼,可以替換掉了,新的代碼如下,可見邏輯已經簡化了,從數組primes中取出來做除法即可:
        // 對數組中的每個數,算出所有質因數,構建map
        for (int i=0;i<nums.length;i++) {
            int cur = nums[i];

            // cur的質因數一定是primes中的一個
            for (int j=0;j<primeNum && primes[j]*primes[j]<=cur;j++) {
                if (cur%primes[j]==0) {
                    map.computeIfAbsent(primes[j], key -> new ArrayList<>()).add(i);

                    // 要從cur中將primes[j]有關的倍數全部剔除,才能檢查下一個素數
                    while (cur%primes[j]==0) {
                        cur /= primes[j];
                    }
                }
            }

            // 能走到這裡依然不等於1,是因為for迴圈中的primes[j]*primes[j]<<=cur導致了部分素數沒有檢查到,
            // 例如6,執行了for迴圈第一輪後,被2除過,cur等於3,此時j=1,那麼primes[j]=3,因此 3*3無法小於cur的3,於是退出for迴圈,
            // 此時cur等於3,應該是個素數,所以nums[i]就能被此時的cur整除,那麼此時的cur就是nums[i]的質因數,也應該放入map
            if (cur>1) {
                map.computeIfAbsent(cur, key -> new ArrayList<>()).add(i);
            }
        }
  • 另外,對於之前99取質因數漏掉了11的問題,上述代碼也有詳細說明:檢查整除結果,大於1的就是漏掉的
  • 完整的提交代碼如下
package practice;

import java.util.*;

/**
 * @program: leetcode
 * @description:
 * @author: [email protected]
 * @create: 2022-06-30 22:33
 **/
public 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;

    // isPrime[3]=0,表示數字3是素數,isPrime[4]=1,表示數字4不是素數
    private static int[] isPrime = new int[100001];

    // 0-100001之間所有的素數都放入這裡
    private static int[] primes = new int[100001];

    // 素數的數量,也就是primes中有效數據的長度
    private static int primeNum = 0;


    static {

        // 歐拉篩
        for(int i=2;i<=100000;i++) {
            if(isPrime[i]==0) {
                // i是素數,就放入primes數組中
                // 更新primes中素數的數量
                primes[primeNum++] = i;
                System.out.println(i + "-" + i*i);
            }

            for(int j=0;i*primes[j]<=100000;j++) {
                // primes[j]*i的結果是個乘積,這樣的數字顯然不是素數,所以在isPrimes數組中標註為1
                isPrime[primes[j]*i] = 1;

                // 如果i是primes中某個素數的倍數,就沒有必要再計算了,退出算下一個,
                // 例如i=8的時候,其實在之前i=4時就已經計算出8不是素數了
                if(i%primes[j]==0) {
                    break;
                }
            }
        }

        // 經過以上代碼,0-100001之間所有素數都放入了primes中
    }


    /**
     * 帶壓縮的並查集查找(即尋找指定數字的根節點)
     * @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];

            // cur的質因數一定是primes中的一個
            for (int j=0;j<primeNum && primes[j]*primes[j]<=cur;j++) {
                if (cur%primes[j]==0) {
                    map.computeIfAbsent(primes[j], key -> new ArrayList<>()).add(i);

                    // 要從cur中將primes[j]有關的倍數全部剔除,才能檢查下一個素數
                    while (cur%primes[j]==0) {
                        cur /= primes[j];
                    }
                }
            }

            // 能走到這裡依然不等於1,是因為for迴圈中的primes[j]*primes[j]<<=cur導致了部分素數沒有檢查到,
            // 例如6,執行了for迴圈第一輪後,被2除過,cur等於3,此時j=1,那麼primes[j]=3,因此 3*3無法小於cur的3,於是退出for迴圈,
            // 此時cur等於3,應該是個素數,所以nums[i]就能被此時的cur整除,那麼此時的cur就是nums[i]的質因數,也應該放入map
            if (cur>1) {
                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;
    }
}
  • 改動完成,提交試試,如下圖,左邊是前文的成績,右邊是本次優化後的成績,從122ms優化到96ms,從超51%優化到超91%,優化效果明顯

在這裡插入圖片描述

  • 至此,《LeetCode952三部曲》全部完成,如果您正在刷題,希望此系列能給您一些參考

歡迎訪問我的GitHub

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

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

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


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

-Advertisement-
Play Games
更多相關文章
  • ## 1 下班前的寂靜 剛準備下班呢,測試大姐又給我提個`bug`,你看我這就操作了一次,`network`里咋有兩個請求? 我心一驚,”不可能啊!我代碼明明就調用一次後端介面,咋可能兩個請求!“。打開她的截圖一看:多個`options`請求。 我不慌不忙解釋道:”這不用管,是瀏覽器預設發送的一個預 ...
  • ## 返回值優化RVO 在cppreference中,是這麼介紹RVO的 `In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, wh ...
  • 背景: 最近在寫一個介面的時候,需求是這樣的,上傳excel,匹配項目有多少個欄位匹配上了,如果匹配上了在單元格上標註綠色背景,然後返回excel文件和匹配的詳細。 首先這個excel文件,後端是不會去保存的,所以無法直接返迴文件鏈接,然後需要返回一個json,告訴前端有多少行是匹配上了的,中匹配多 ...
  • # 02-針對商品排行榜,你是怎麼實現的 ## 背景描述 當時產品提出了每日熱銷排行榜在零點進行變更的需求。在我接到這個需求後,我立即想到了使用Redis的有序集合(ZSET)來實現這個功能,並與我們的技術負責人進行了溝通。 經過與技術負責人的討論和確認,我們一致認為使用有序集合是一個可行的解決方案 ...
  • ## switch語句 使用switch語句來選擇要執行的多個代碼塊中的一個。 在Go中的switch語句類似於C、C++、Java、JavaScript和PHP中的switch語句。不同之處在於它只執行匹配的case,因此不需要使用break語句。 單一case的switch語法 ```Go sw ...
  • ## 1、條件變數 當線程需要等待特定事件發生、或是某個條件成立時,可以使用條件變數`std::condition_variable`,它在標準庫頭文件``內聲明。 ```c++ std::mutex mut; std::queue data_queue; std::condition_variab ...
  • # 學習Markdown Typora初體驗 ## 標題 語法:`#+空格+標題+回車`。幾級標題就有幾個#號。 ### 三級標題 #### 四級標題 ##### 五級標題 ###### 六級標題 ####### 七級標題。。。我沒有 ## 字體 *斜體* 語法: $$ *內容* $$ **加粗** ...
  • # 第一章HTML #### 1.1 html的定義 html是超文本標記語言,是一個基於HTTP(超文本傳輸協議)協議的網頁語言 #### 1.2 html的版本 HTML 4.01 以及具備完善的網頁編輯 HTML 5.0 移動端網頁編輯 XHTML 語法嚴格 #### 1.3 瀏覽器 保障相容 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...