日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

c++如何输入数组_从一个数组中找出 N 个数,其和为 M 的所有可能最 nice 的解法...

發布時間:2023/12/19 c/c++ 31 豆豆
生活随笔 收集整理的這篇文章主要介紹了 c++如何输入数组_从一个数组中找出 N 个数,其和为 M 的所有可能最 nice 的解法... 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

編者按:本文由前端狂想錄公眾號授權奇舞周刊轉載。

故事的背景

這是一個呆萌炫酷吊炸天的前端算法題,曾經乃至現在也是叱咤風云在各個面試場景中。

可以這樣說,有 90% 以上的前端工程師不會做這個題目。

這道題涉及的知識點很多,雖然網上也有相關的解答文章,但是在算法小分隊的討論和分析中,一致認為網上的文章太舊了,而且最多就是貼貼代碼,寫寫注釋,并沒有具體的分析。

暴風雨前的寧靜

我們看一下題目:

從一個數組中找出 N 個數,其和為 M 的所有可能。

大家可以中斷 5 分鐘想一下,不管想什么,反正先看著題目想 5 分鐘。

想完了嗎?

是不是感受到了一股殺氣,好像知道些,但是又無從下手的那種勝利的感覺,嗯...?

機智(雞賊)的我,選擇先把題目留在這兒,我們先去探討一下算法。

為什么算法就可以無法無天?

關于上面這個無解的問題,我們默默不說話。現在,我們來思考幾個簡單點的哲學問題:

什么是算法?

算法的作用和目的是什么?

如何設計我們的算法?

算法的復雜度該如何表示?

如何測試和優化我們的算法?

嗯?我怎么感覺死循環了呢?無限遞歸且沒有終止條件?

什么是算法

《算法導論》一書將算法( algorithm )描述為定義良好的計算過程,它取一個或一組值作為輸入,并產生一個或一組值作為輸出。

計算的組成

計算機主要的單元包括:I/O 、CPU 、內存等,這里我們要介紹的是CPU。

CPU 負責的功能主要就是解釋和執行程序,它能認識的就是一堆 0 和 1 組成的機器碼。

我們敲下的每一行代碼最終都被編譯成機器碼,然后將機器碼交給 CPU 執行。

想一想上面的話,我們可以知道:

我們所知的程序,其實就是指令和數據的集合,而算法的本質就是執行設計好的指令集,從輸入到產生輸出的過程。

算法是抽象的概念,但越是抽象的東西,其越具有清晰的特征。特征如下:

  • 確定性: 算法的每一個步驟都是明確的、可行的、結果可預期的

  • 有窮性: 算法要有一個終止條件

  • 輸入和輸出: 算法是用來解決問題的,少不了輸入和輸出

  • 大多數人只看見了樹,卻未見森林

    看到這,你可能有疑問,為什么要這樣說呢?且聽我娓娓道來:

    列舉一些大家可能知道的一些詞吧:

    遞歸法、貪婪法、分治法、動態規劃法、線性規劃法、搜索和枚舉法(包括窮盡枚舉)、極大極小值法、 Alpha-beta 、剪枝等等。

    看到上面這些稀罕詞,很多人認為這就是算法了,但其實這只是算法設計范式中,一些前人總結出來的模式而已。

    我們可以將這種算法層面的模式和平常我們說的設計模式進行對比。

    對比后,會發現,這就是一種算法抽象的最佳實踐范式。其實我們寫的任何一個代碼片段(包含輸入和輸出),都可以認為是算法的子集,甚至程序也只是算法的一種存在形式而已。

    之前看到有人把程序比做水流,從源頭順流而下(順序執行),也可以分流而下(分支、并發),還可以起個漩渦(遞歸),其實這些也都只是算法中具體的實現或者組織方式而已。

    算法的領域極其廣闊,不要把思維僅僅局限在計算機領域中。

    對于計算機行業人員來說,算法就是內功。就好比乾坤大挪移,學成之后,天下武功皆能快速掌握。

    算法設計

    這一塊兒其實是很龐大的知識體系,需要苦練內功根基。下面簡要介紹下算法設計方面的知識。

    順序執行、循環和分支跳轉是程序設計的三大基本結構。

    算法也是程序,千姿百態的算法也是由這三大基礎結構構成的。

    算法和數據結構關系緊密,數據結構是算法設計的基礎。

    如果對諸如哈希表、隊列、樹、圖等數據結構有深刻的認識,那在算法設計上將會事半功倍。

    上面提到的知識,主要的目的是拋磚引玉。算法的設計與分析是無上神功的心法口訣和入門要領。無論多么精妙絕倫的算法實現,都是由一些最基礎的模型和范式組裝起來的。

    關于算法設計,這里給大家推薦一門課程,很不錯,小伙伴可以看看:

    算法設計與分析-Design and Analysis of Algorithms(https://zh.coursera.org/learn/algorithms)

    TIPS: 小伙伴如果有好的資源,也可以留言 mark 哦。

    最?NICE?的解法

    降維分析,化繁為簡

    現在,到了最關鍵的時刻。我們回到題目中,開始設計我們的算法。

    題干信息很簡單,核心問題在于:

    如何從數組中選取?N?個數進行求和運算。

    如何做,這里我們通常且正確的做法,是對問題進行降維分析,并且化繁為簡。

    下面開始降維分析,化繁為簡:

    假如 N = 2 ,也就是找出數組中兩個數的和為 M 的話,你會怎么做?可能你會想到每次從數組中彈出一個數,然后與余下的每個數進行相加,最后做判斷。

    那么問題來了,當 N = 3 呢,N = 10 呢,會發現運算量越來越大,之前的方式已經不可行了。

    不妨換一種思路:

    數組中選取不固定數值 N ,我們可以嘗試著使用標記的方式,我們把 1 表示成選取狀態, 把 0 表示成未選取狀態。

    假設數組 const arr=[1,2,3,4] ,對應著每個元素都有標記 0 或者 1 。如果 N=4 ,也就是在這個數組中,需要選擇 4 個元素,那么對應的標記就只有一種可能 1111 ,如果 N=3 ,那就有 4 種可能,分別是 1110 、 1101 、1011 以及 0111 (也就是 C4取3->4 ) 種可能。

    開始抽象

    通過上面的層層敘述,我們現在的問題可以抽象為:

    標記中有幾個 1 就是代表選取了幾個數,然后再去遍歷這些 1 所有可能存在的排列方式,最后做一個判斷,這個判斷就是:每一種排列方式,都代表著數組中不同位置的被選中的數的組合,所以這里就是將選中的這些數字,進行求和運算,然后判斷求出的和是不是等于 M 。

    于是,問題開始變得簡單了。

    如何將數組和標記關聯

    0101?這樣的數據一眼望上去第一反應就是二進制啊

    對于 arr 來說,有 4 個元素,對應的選擇方式就是從 0000( N = 0 )到 1111( N = 4 )的所有可能。

    而 1111 就是 15 的二進制,也就是說這所有的可能其實對應的就是 0 - 15 中所有數對應的二進制。

    這里的問題最終變成了如何從數組長度?4?推導出?0 - 15

    這里采用了位運算--左移運算, 1 << 4 的結果是 16 。

    所以我們可以建立這樣一個迭代:

    const?arr?=?[1,?2,?3,?4]

    let?len?=?arr.length,?bit?=?1?<<?len

    // 這里忽略了 0 的情況(N = 0),取值就是 1 - 15

    for(let?i?=?1;?i?<?bit;?i++)?{

    ??// ...

    }

    如何從?1110?標記中取出?1?的個數

    最簡單的方式:

    const?n?=?num?=>?num.toString(2).replace(/0/g,?'').length

    這其實也是一道算法常考題,因為位運算是不需要編譯的,肯定速度最快。

    PS: 如果不理解位運算為何會提高性能的同學,可以自行搜索一下位運算。簡單點說就是:位運算直接用二進制進行表示,省去了中間過程的各種復雜轉換,提高了速度。

    我們嘗試使用?&?運算來解決這個問題

    首先我們肯定知道 1 & 1 = 1; 1 & 0 = 0 這些結論的。所以我們從 15 & 14 => 14 可以推導出 1111 & 1110 => 1110 ,為什么可以這樣推導呢,因為 15 的二進制就是 1111 ,14 同理。

    我們可以看到,通過上面的操作消掉了最后的 1。

    所以我們可以建立一個迭代,通過統計消除的次數,就能確定最終有幾個 1 了。

    代碼如下:

    const?n?=?num?=>?{

    ??let?count?=?0

    ??while(num)?{

    ????num?&=?(num?-?1)

    ????count++

    ??}

    ??return?count

    }

    計算和等于?M

    現在我們已經可以把所有的選取可能轉變為遍歷一個數組,然后通過迭代數組中的每個數對應的二進制,有幾個 1 來確定選取元素的個數。

    那么,現在需要的最后一層判斷就是選取的這些數字和必須等于?M

    這里其實就是建立一個映射:

    1110 到 [1, 2, 3, 4] 的映射,就代表選取了 2, 3, 4,然后判斷 2 + 3 + 4 與 M 。

    這里可以這樣看:1110 中的左邊第一個 1 對應著數組 [1, 2, 3, 4] 中的 4 。

    現在有一個問題,該如何建立這個映射關系呢?

    我們知道前者 1110 其實就是對應的外層遍歷中的 i = 14 的情況。

    再看看數組[1, 2, 3, 4] ,我們可以將元素及其位置分別映射為 1000 0100 0010 0001。

    實現方式也是通過位運算--左位移來實現:

    1 << inx ,inx 為數組的下標。

    位掩碼介紹

    位掩碼 不熟悉的童鞋會有點暈,這里簡單科普下:

    實質上,這里的 1 << j ,是指使用 1 的移位來生成其中僅設置第 j 位的位掩碼。

    比如:14 的二進制表示為 1110,其代表(從右往左)選取了第 2 , 3 , 4 位。

    那么(下面故意寫成上下對應的方式):

    // demo1

    ??1110

    &

    ??0001

    =

    ??0000

    // demo2

    ??1110

    &

    ??0010

    =

    ??0010

    PS: 通過上面代碼,我們可以看到上下對應的 0 和 1 在進行 & 運算以后,得出的結果和在 js 中進行相同條件下 & 運算的結果相同。

    所以:

    1?<<?0?// 1 -> 0001

    1?<<?1?// 2 -> 0010

    1?<<?2?// 4 -> 0100

    1?<<?3?// 8 -> 1000

    // 說白了,就是把左邊的值變成二進制形式,然后左移或者右移,超出補0

    // 所以, 1110 對應著 第一位沒有選取,那么 1110 & 0001(設置為第一位的位掩碼) = 0,如果 i & (1 << inx) !== 0 代表該位被選取了

    for(let?j?=?0;?j?<?arr.length;?j++){

    ??if((i?&?(1?<<?j)?!==?0)?{

    ????// 代表這個數被選取了,我們做累加求和就行

    ??}

    }

    所以綜上所述,最終代碼實現如下:

    // 參數依次為目標數組、選取元素數目、目標和

    const?search?=?(arr,?count,?sum)?=>?{

    ??// 計算某選擇情況下有幾個 `1`,也就是選擇元素的個數

    ??const?n?=?num?=>?{

    ????let?count?=?0

    ????while(num)?{

    ??????num?&=?(num?-?1)

    ??????count++

    ????}

    ????return?count

    ??}

    ??let?len?=?arr.length,?bit?=?1?<<?len,?res?=?[]

    ??// 遍歷所有的選擇情況

    ??for(let?i?=?1;?i?<?bit;?i++){

    ????// 滿足選擇的元素個數 === count

    ????if(n(i)?===?count){

    ??????let?s?=?0,?temp?=?[]

    ??????// 每一種滿足個數為 N 的選擇情況下,繼續判斷是否滿足 和為 M

    ??????for(let?j?=?0;?j?<?len;?j++){

    ????????// 建立映射,找出選擇位上的元素

    ????????if((i?&?1?<<?j)?!==?0)?{

    ??????????s?+=?arr[j]

    ??????????temp.push(arr[j])

    ????????}

    ??????}

    ??????// 如果這種選擇情況滿足和為 M

    ??????if(s?===?sum)?{

    ????????res.push(temp)

    ??????}

    ????}

    ??}

    ??return?res

    }

    如何測試

    這其實也是可以單獨寫一篇文章的知識點。

    測試的種類、方式多種多樣,我們將自己想象成一個 TroubleMaker ,各種為難自己寫的算法,然后不斷優化自己的代碼,這個過程也是有趣極了。

    首先二話不說,照著心里一開始就想的最簡單的方式擼一個測試用例。

    代碼如下:

    // 寫一個很大的數組進行測試

    const?arr?=?Array.from({length:?10000000},?(item,?index)?=>?index)

    // 測試不同選取容量

    const?mocks?=?sum?=>?[3,?300,?3000,?30000,?300000,?3000000].map(item?=>?({count:?item,?sum?}))

    let?res?=?[]

    mocks(3000).forEach((count,?sum)?=>?{

    ??const?start?=?window.performance.now()

    ??search(arr,?count,?sum)

    ??const?end?=?window.performance.now()

    ??res.push(end?-?start)

    })

    然后結果如下圖:

    發現造了一個長度為 1000 萬的數組,找 6 個數,居然只要 0.014 秒。什么鬼,這么快的么,開掛了吧。不行,感覺這不靠譜,還是從長計議,寫一個專業的測試案例比較好,請繼續往下看。

    我們主要從兩個方向下手:

    第一個方向:全方位攻擊

    其實就是摳腦袋想出一萬種情況去折騰自己的代碼,也就是所謂的 地毯式測試案例轟炸。

    完整代碼如下:

    // 比如針對上面的算法,可以這樣寫

    export?const?assert?=?(desc,?condition)?=>?{

    ??condition?=?typeof?condition?===?"function"???condition()?:?condition;

    ??console.info(`[Test] %c${desc}`,?"color: orange");

    ??if?(!condition)?{

    ????throw?new?Error(`[Error] %c${desc}?failed.`,?"color: pink");

    ??}

    ??console.info(`[Success] %c${desc}?success.`,?"color: #198");

    };

    const?mock_a?=?Array.from({?length:?4?},?(item,?index)?=>?index);

    const?mock_b?=?Array.from({?length:?6?},?(item,?index)?=>?index?-?3);

    const?mock_c?=?Array.from({?length:?4?},?()?=>?0);

    assert(`正整數情況測試`,?()?=>?{

    ??const?res?=?search(mock_a,?2,?3);

    ??const?lengthTest?=?res.length?===?2;

    ??const?resTest?=?JSON.stringify(res)?===?JSON.stringify([[1,?2],?[0,?3]]);

    ??return?lengthTest?&&?resTest;

    });

    assert(`負數情況測試`,?()?=>?{

    ??const?res?=?search(mock_b,?2,?0);

    ??const?lengthTest?=?res.length?===?2;

    ??const?resTest?=?JSON.stringify(res)?===?JSON.stringify([[-1,?1],?[-2,?2]]);

    ??return?lengthTest?&&?resTest;

    });

    // ...

    codesandbox 完整代碼地址(https://codesandbox.io/s/38pn0jvmr1)

    在 codesandbox 的運行控制臺下,可以看到測試結果如下圖所示:

    會發現,這里把很多測試場景都覆蓋了,這樣的好處就是讓自己的代碼能夠更魯棒、更安全。

    當然,上面的 case 還可以再極端點,從而可以驅使代碼變得更加魯棒。

    極端的場景帶來的優化如下:

    第一個極端:增加小數的情況。因為精度誤差的問題,這里就可以將代碼繼續優化,以支持小數情況。

    第二個極端:增加公差參數的情況。不用絕對相等來取數,可以通過公差來增加靈活性。

    第二個方向:性能測試和持續優化

    關注的第二個點在于性能測試,為什么呢?

    如果要將自己的算法付諸生產,除需要穩定的功能表現之外,還要有足夠讓人信服的性能。畢竟,性能高的代碼是我們普遍的追求。

    這里分享一下有用的技巧:

    通過 window.performance.now() ,或者 console.time() ,我們可以簡單快捷的獲取到程序執行的時間。

    在寫這樣的測試案例的時候,有幾個原則,分別是:

    只關注自己的測試內容。

    保持干凈

    考慮極端情況(比如數組很大很大)

    將多次測試結果進行對比分析

    當然你也可以使用類似 jsperf 之類的工具,然后一點一點的去扣這些優化點,來持續打磨自己的代碼。

    這里提一下,雖然通過測試數據的反饋,可以調整你的算法。但是不要為了性能盲目的進行優化,性能優化其實就是找一個平衡點,原則是足夠用就好。

    具體怎么做呢?對于上面的算法,我們如果采用空間換時間的優化方式,可以在計算 1 的個數的 n 函數上做一些優化 ,比如加個緩存。

    然后對比性能,如下圖所示::

    完整的測試案例地址(https://jsperf.com/acm-perf/1)

    測試性能對比

    說到測試性能對比,這里主要收集一些實現方式,比如基于 jsperf 做一些對比。

    如果有其他的實現方式,歡迎小伙伴留言補充。

    jsperf 測試性能結果,如下圖所示::

    完整的測試案例地址(https://jsperf.com/acm-r/1)

    總結

    這道題很有難度,用到的知識也很多,認真想一想,一定能有很多收獲的,這道算題的解決,大致用到了以下這些知識:

  • 二進制以及位運算的知識

  • 剪枝的思想

  • 如何全方位、地毯式、走極端、多方面的對一個算法進行性能測試

  • 算法思想,算法設計相關的簡要知識

  • 如何將極其抽象的問題進行降維分析,化繁為簡

  • 番外篇--復雜度

    算法復雜度

    這涉及到大 O 判斷法。(算法業內一般不叫大 O ,叫 big O 連起來讀,比如 B溝N )

    這部分內容大家自行維基搜索吧,很詳細的。下面我們提一下,離我們工作很近的一種復雜度的計算法則。

    如何判斷業務代碼的復雜度

    對前端來說,大 O 判斷法不能太適用,其實還有一種很有趣的判斷復雜度的方法,它叫做 Tom McCabe 方法。

    該方法很簡單,通過計算函數中 決策點 的數量來衡量復雜度。下面是一種計算決策點(結合前端)的方法:

  • 從 1 開始,一直往下通過函數

  • 一旦遇到 if while for else 或者帶有循環的高階函數,比如 forEach map 等就加 1

  • 給 case 語句中的每一種情況都加 1

  • 比如下面代碼:

    function?fun(arr,?n)?{

    ??let?len?=?arr.length

    ??for?(let?i?=?0;?i?<?len;?i++)?{

    ????if?(arr[i]?==?n)?{

    ????????// todo...

    ????}?else?{

    ????????// todo...

    ????}

    ??}

    }

    按照 Tom McCabe 方法來計算復雜度,那么這個 fun 函數的決策點數量是 3 。知道決策點數量后,怎么知道其度量結果呢?這里有一個數值區間判斷:

    數量區間度量結果
    0-5這個函數可能還不錯
    6-10得想辦法簡化這個函數了
    10+把這個函數的某一部分拆分成另一個函數并調用他

    從上面的判斷依據可以看出,我上面寫的代碼,數量是 3 ,可以稱為函數還不錯。我個人覺得這個判斷方法還是很科學和簡單的,可以作為平時寫代碼時判斷函數需不需要重構的一個考慮點,畢竟一個函數,一眼掃去,決策點數量大致就知道是多少了,很好計算。

    感謝付出的小伙伴

    非常感謝前端算法小分隊的成員一起努力攻克了這個題目。

    特此感謝:

    本文第一作者:小金剛(大佬)

    github 地址:https://github.com/cbbfcd

    掘金地址:https://juejin.im/user/593df367128fe1006aecb3cf

    文章排版和內容深度完善:源碼終結者

    github 地址:https://github.com/godkun

    參與審校和核對:Ninja

    github 地址:https://github.com/jiameng123

    其他小伙伴的貢獻:前端狂想錄算法小分隊整體成員

    編者:此外還有一篇《改進,從一個數組中找出 N 個數,其和為 M 的所有可能》(https://juejin.im/post/5c81fee66fb9a049b82b4128),采用的是二進制正序表示法,各位同學可以對照看一下。

    關于奇舞周刊

    《奇舞周刊》是360公司專業前端團隊「奇舞團」運營的前端技術社區。關注公眾號后,直接發送鏈接到后臺即可給我們投稿。

    總結

    以上是生活随笔為你收集整理的c++如何输入数组_从一个数组中找出 N 个数,其和为 M 的所有可能最 nice 的解法...的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。