毎日のアルゴリズム: 上位 K 個の高頻度要素

毎日のアルゴリズム: 上位 K 個の高頻度要素

空でない整数の配列が与えられた場合、最も頻繁に出現する上位 k 個の要素を返します。

例1:

  1. 入力: nums = [1,1,1,2,2,3]、k = 2
  2. 出力: [1,2]

例2:

  1. 入力: nums = [1]、k = 1
  2. 出力: [1]

ヒント:

  • 与えられた k は常に妥当であり、1 ≤ k ≤ 配列内の一意の要素の数であると想定できます。
  • アルゴリズムの時間計算量は O(nlogn) より小さくなければなりません。ここで n は配列のサイズです。
  • 質問データは、回答が一意であることを保証します。つまり、配列の最初の k 個の高頻度要素のセットは一意です。
  • 回答は任意の順序で返すことができます。

解決策1: マップ+配列

マップを使用して各要素の頻度を記録し、配列を使用して要素を比較および並べ替えます。

コード実装:

  1. topKFrequent =関数(nums, k) {
  2. map = new Map(), arr = [...新しいSet (nums)] とします。
  3. nums.map((num) => {
  4. map.has(num) の場合、 map.set (num, map.get(num)+1) を実行します。
  5. それ以外の場合はmap.set (num, 1)
  6. })
  7. arr.sort((a, b) => map.get(b) - map.get(a)).slice(0, k);を返します
  8. };

複雑性分析:

  • 時間計算量: O(nlogn)
  • 空間計算量: O(n)

質問では、アルゴリズムの時間計算量が O(n log n) より優れている必要があるため、この実装は質問の要件を満たしていません。

解決策 2: マップ + 小さなトップヒープ

配列を走査して各要素の頻度をカウントし、要素の値(キー)と出現頻度(値)をマップに保存します。

マップデータを通じて、上位 k 個の高頻度要素の小さなトップヒープが構築されます。小さなトップヒープ上の任意のノードの値は、その左と右の子ノードの値以下である必要があります。つまり、ヒープの上部が最小値です。

具体的な手順は次のとおりです。

  • データを走査し、各要素の頻度をカウントし、要素の値(キー)と出現頻度(値)をマップに保存します。
  • マップを横断し、最初のk個の数字で小さなトップヒープを構築します。
  • 位置 k から始めて、マップのトラバースを続けます。各データの発生頻度は、小さなトップ ヒープの最上位要素の発生頻度と比較されます。最上位要素より小さい場合は、処理は行われず、次の要素へのトラバースが続行されます。最上位要素より大きい場合は、この要素がヒープの最上位要素を置き換え、ヒープが小さなトップ ヒープに変換されます。
  • トラバーサルが完了すると、ヒープ内のデータは上位k個の最大データになります。

コード実装:

  1. topKFrequent =関数(nums, k) {
  2. map = new Map()、heap = [,] とします。
  3. nums.map((num) => {
  4. map.has(num) の場合、 map.set (num, map.get(num)+1) を実行します。
  5. それ以外の場合はmap.set (num, 1)
  6. })
  7. // 要素数がk以下の場合
  8. マップのサイズがk以下の場合
  9. [...map.keys()]を返す
  10. }
  11. // 要素数が k より大きい場合は、マップをトラバースして小さなトップヒープを構築します
  12. i = 0とする
  13. map.forEach((値、キー) => {
  14. i < k の場合 {
  15. // 最初の k を取得してヒープを構築し、それをヒープに挿入します
  16. heap.push(キー)
  17. // 最初の k 個のヒープをその場で作成します
  18. if(i === k-1) ヒープを構築(ヒープ、マップ、k)
  19. }そうでない場合、map.get(heap[1]) < 値) {
  20. // 置き換えてスタックする
  21. ヒープ[1] =キー
  22. // 最初の要素を上から下に積み重ねる
  23. ヒープ化(ヒープ, マップ, k, 1)
  24. }
  25. 私は++
  26. })
  27. // ヒープの最初の要素を削除します
  28. ヒープ.shift()
  29. ヒープを返す
  30. };
  31. // 後ろから前へ、上から下へ、その場でヒープを構築します
  32. buildHeap = (ヒープ、マップ、k) => {
  33. if(k === 1)戻り値
  34. // 最後の非リーフノードから始めて上から下に積み重ねます
  35. (i = Math.floor(k/2); i>=1; i --)の場合{
  36. ヒープ化(ヒープ, マップ, k, i)
  37. }
  38. }
  39. // ヒープ
  40. ヒープ化 = (ヒープ、マップ、k、i) => {
  41. // トップダウンスタッキング
  42. while( true ) {
  43. minIndex = i とします
  44. 2*i <= k && map.get(heap[2*i]) < map.get(heap[i])) {
  45. 最小インデックス = 2*i
  46. }
  47. if(2*i+1 <= k && map.get(heap[2*i+1]) < map.get(heap[minIndex])) {
  48. 最小インデックス = 2*i+1
  49. }
  50. 最小インデックスが i の場合
  51. swap(ヒープ, i, minIndex)
  52. i = 最小インデックス
  53. }それ以外{
  54. 壊す
  55. }
  56. }
  57. }
  58. // 交換
  59. swap = (arr, i, j) => {とします。
  60. temp = arr[i]とします。
  61. arr[i] = arr[j]
  62. arr[j] =一時
  63. }

複雑性分析:

  • 時間計算量: 配列の走査には O(n) の時間計算量が必要で、ヒープ化には O(logk) の時間計算量が必要であるため、ヒープを使用して Top k 問題を見つける時間計算量は O(nlogk) です。
  • 空間計算量: O(n)

解決策3: バケットソート

ここでは、最初のk個の高頻度要素を取得する必要があり、カウンティングソートはもはや適していません。前の質問では、カウンティングソートを使用して、要素iがbucket[i]に出現した回数を保存しましたが、この保存ではバケット配列上の値が順番になっていることを保証できません。たとえば、bucket = [0,3,1,2]、つまり要素0は出現せず、要素1は3回出現し、要素2は1回出現し、要素3は2回出現します。したがって、カウンティングソートは最初のk個の高頻度要素を取得するのに適していません。ただし、カウンティングソートが機能しない場合は、バケットソートがあるので心配しないでください。

バケットソートは、カウントソートのアップグレード版です。また、関数のマッピング関係も活用します。

バケット ソートは、入力データが均一に分散されていると想定し、データを有限数のバケットに分割し、各バケットを個別にソートすることによって機能します (別のソート アルゴリズムを使用することも、バケット ソートを再帰的に使用し続けることもできます)。

  • まずマップを使用して頻度を保存します
  • 次に、配列(特定の数のバケットを持つ)を作成し、頻度を配列の添え字として使用して、異なる頻度を持つ数値セットを対応する配列の添え字(バケット内)に格納します。

コード実装:

  1. topKFrequent =関数(nums, k) {
  2. map = new Map(), arr = [...新しいSet (nums)] とします。
  3. nums.map((num) => {
  4. map.has(num) の場合、 map.set (num, map.get(num)+1) を実行します。
  5. それ以外の場合はmap.set (num, 1)
  6. })
  7. // 要素数がk以下の場合
  8. マップのサイズがk以下の場合
  9. [...map.keys()]を返す
  10. }
  11. バケットソート(map, k)を返す
  12. };
  13. // バケットソート
  14. バケットソート = (map, k) => {
  15. arr = []、res = []とします。
  16. map.forEach((値、キー) => {
  17. // マッピング関係(出現頻度を添え字とする)を使用して、各バケットにデータを分配します
  18. if(!arr[値]) {
  19. arr[値] = [キー]
  20. }それ以外{
  21. arr[値].push(キー)
  22. }
  23. })
  24. // 逆順に走査して、最も頻度の高い最初の k 個の数値を取得します。
  25. (i = arr.length - 1;i >= 0 && res.length < k;i --) {
  26. if(arr[i]) {
  27. res.push(...arr[i])
  28. }
  29. }
  30. 戻り
  31. }

複雑性分析:

  • 時間計算量: O(n)
  • 空間計算量: O(n)

リートコード: https://leetcode-cn.com/problems/top-k-frequent-elements/solution/javascript-qian-k-ge-gao-pin-yuan-su-by-user7746o/

<<:  あなたのビジネスに必要な AI 処理ユニットはどれですか?

>>:  フロントエンドではアルゴリズムを理解する必要はないと思いますか?実際の例を見てみましょう。

ブログ    

推薦する

ポストSORA時代において、CV実践者はどのようにモデルを選択するのでしょうか?畳み込みまたはViT、教師あり学習またはCLIPパラダイム

ImageNet の精度は常にモデルのパフォーマンスを評価するための主要な指標であり、ディープラーニ...

...

...

データマイニングのためのK平均法アルゴリズムのグラフィカルな説明

K-means クラスタリング アルゴリズム 中国語名は「K-means クラスタリング アルゴリズ...

...

ベクトルデータベースが生成AIを強化する方法

ベクトル データベースは、LLM と外部情報の間のブリッジとして機能し、生成 AI システムの基本機...

人間や魚を認識するAIは人魚も認識できるのか? Alibaba CVPR 論文における因果推論法の回答

[[399013]]人間と魚の写真で訓練された AI は、初めて人魚の写真を見たときにどのように反応...

NeRFは過去のものになるのか?立体復元は3D GSの新時代へ! (復旦大学からの最新レビュー)

この記事は、Heart of Autonomous Driving の公開アカウントから許可を得て転...

信頼できるAIの基礎は、適切なタイミングで適切なデータを得ることです

私たちは人工知能の存在に慣れ始めており、生成型人工知能(GenAI)の普及により、人工知能が世界に与...

C# で開発されたソートアルゴリズムの詳細な説明

C# 言語は、まだ比較的一般的なものです。ここでは、バブル ソート、選択ソート、挿入ソート、シェル ...

ニューラルネットワークが大きいほど良いのはなぜですか? NeurIPSの論文が証明:堅牢性は一般化の基礎である

ニューラルネットワークの研究方向が徐々に超大規模な事前トレーニング済みモデルへと移行するにつれて、研...

データセットと DataLoader を使用して PyTorch でデータをカスタマイズする

大規模なデータセットを扱う場合、データ全体を一度にメモリにロードすることが非常に困難になることがあり...

統計分析と人工知能の9つの有名な大惨事

2017年、『エコノミスト』誌は、石油ではなくデータが世界で最も価値のある資源になったと宣言しました...

「百度脳産業イノベーションフォーラム」が本格始動、伝統産業向けAIソリューションを提案

「将来、AIとは何の関係もないと主張する企業はなくなるだろう」これは、2018年の世界人工知能会議で...