LeetCode49、1つの質問でハッシュアルゴリズムを学ぶ

この記事は個人のパブリックアカウントから作成されました:TechFlow、オリジナルは簡単ではありません、注意を求めてください


今日はLeetCodeトピックの30番目の記事です。文字列のグループ化の問題を見てみましょう

タイトル

この質問の意味は非常に単純です。文字列の配列を考えると、すべての文字列をその構成に従ってグループ化する必要があります

たとえば、指定された配列が[eat、ate、tea、tan、nat、bat]の場合。

このうち、eat、ate、teaの3つの単語に使用されている文字は、それぞれe、t、aです。tanとnatはどちらもa、n、tを使用し、最後にbatが残るため、グループ化の結果は[eat、ate、tea]、[tan、nat]、[bat]になります。

暴力

グループ化の基本は、各文字列で使用される文字の状況ですそのため、各文字列のすべての要素を分解してdictに入れることができます。次に、このdictをグループ化の標準として使用し、同じdictの文字列を同じグループに入れます。

たとえば、{'e':1、 'a':1、 't':1}に変更します。1つの文字が複数回出現する場合があるため、出現回数も記録する必要があります。しかし、問題があります。dictは動的データです。Pythonでは、別のdictのキーとしてそれを使用できませんこの問題を解決する最も簡単な方法は、この辞書を 'e1a1t1'などの文字列結合するメソッドを記述することです。これをキーとして使用します。しかし、別の問題があります。dict内のキーは必ずしも順序付けされていないため、dictをソートする必要があります。下の図にフローを示します。

つまり、入力が文字列で出力がこの文字列の要素である関数を実装する必要があります。

def splitStr(s):
    d = defaultdict(int)
    for c in s:
        d[c] += 1
    ret = ''
    # 将dict中内容排序
    for k,v in sorted(d.items()):
        ret += (str(k) + str(v))
    return ret

ここでは簡単です。グループ化された結果格納するために外側のレイヤーにdictを作成できます。コードを簡単に記述できます。

from collections import defaultdict

class Solution:
    def splitStr(self, s):
        d = defaultdict(int)
        # 拆分字符串中元素
        for c in s:
            d[c] += 1
        ret = ''
        # 将dict中内容排序
        for k,v in sorted(d.items()):
            ret += (str(k) + str(v))
        return ret
    
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        groups = defaultdict(list)
        for s in strs:
            # 拿到拆分之后的字符串作为key进行分组
            key = self.splitStr(s)
            groups[key].append(s)
            
        ret = []
        for _, v in groups.items():
            ret.append(v)
        return ret
        

一部の友人は問題を認識している可能性があります。最初にそれを辞書に変換し、その後それを文字列にスプライスする必要があるので、なぜ文字列を直接並べ替え、並べ替えた結果をキーとして使用しないのですか?同じ構成要素を持つ文字列は、ソート後に同じ結果になる必要があります。

たとえば、並べ替え後、リンゴとpplaeはどちらもaelppですが、これは可能ですか?

アイデアは大丈夫​​ですが、提出は合格しません。その理由も非常に単純で、3つの単語を要約できますつまり、複雑さです。文字列の長さが固定されていないため、これを行うのは非常に複雑であり、文字列を1つずつ並べ替えるには多くのオーバーヘッドが必要です。さらに、ソートされた結果をキーとして使用します。これは、ストレージリソースも占有します。したがって、これは良い方法ではありません。

ハッシュ

次に、私たちは私たちのメインプレーに来ました、誰もがタイトルから見るべきでした、この質問はハッシュアルゴリズムに関連しています

合理的に言うと、ハッシュアルゴリズムの名前は、よく耳にするLei Guanerのような名前ですが、多くの人はハッシュアルゴリズムの機能や使用する場所を知りません。あなたがもっと聞くかもしれないのはhashMapです。

実際、ハッシュアルゴリズムの内容は非常に単純で、単にマップとして理解できます入力は何でもかまいません。数値、配列、オブジェクトのいずれでもかまいませんが、出力は固定バイト数の情報です。たとえば、次の図のモジュロ4はハッシュ関数ですが、モジュロ4の結果に従って数値を異なるバケットに分類できます。

私たちは、私たちは、同じサブバレルでそれらを分割するに応じてデータの一部にすることができ、各データのハッシュができるだけ同じではありません後に結果を作ることができますそうすることで、達成することである圧縮情報をたとえば、サイズが2MBのインスタンスをハッシュし、32ビットの文字列を取得しました。これは、32ビット文字列を使用して元の2MBコンテンツを表すのと同じであり、効率的なクエリやその他の操作を実行できます。たとえば、現在の顔認識モジュールは、ハッシュ関数として簡単に理解できます。カメラが写真を撮影し、アルゴリズムが写真をIDにハッシュし、データベースに移動してこのIDに対応する個人情報を検索し、識別プロセスを完了します。

この問題では、文字列の内容応じて文字列とハッシュを読み取り、同じ文字列ハッシュを作成して得られた結果の一貫性を確保するハッシュ関数を設計したいと考えています。このハッシュ後の結果をバケットに使用します基本的に、上記のアプローチはハッシュ方式と見なすこともできます。ただし、これにはソートが含まれるため、少し複雑になり、最終結果は文字列になります。時間の複雑さと空間の複雑さの観点から、最適化の余地はまだあります。以下では、より一般的に使用されるハッシュアルゴリズムを見ていきます。

このアルゴリズムでは、ハッシュファクターとして素数を選択するため、ハッシュの衝突の確率は比較的低くなります。素数の力でハッシュ結果を構成するために、コードを見てみましょう:

def hash(dict):
    ret = 0
    prime = 23
    # k代表字符,v表示出现次数
    for k, v in dict:
        ret += v * pow(23, ord(k) - ord('a'))
        # 对2^32取模,控制返回值在32个bit内
        ret %= (1 << 32)
    return ret

ここのordは、ASCIIコードを取得する操作です。つまり、英字を数字に変換します。異なる素数の特定のべき乗使用して異なる文字を表し、文字が係数として現れる回数を乗算します。最後に、各文字のハッシュ値が累積され、文字列全体のハッシュ値が取得されます。同じ文字列の係数とべき乗は同じであるため、最終的な合計結果は明らかに等しくなります。この結論は問題ありません。しかし、逆に、ハッシュ値が等しい文字列は本当に同じですか?

実際に考えると、反例を考えることができます。たとえば、1つの文字aがある場合、そのハッシュ値は 1 2 0 = 1 1 * 23 ^ 0 = 1 、単一のbのハッシュ値も非常によく、23です。23 aのハッシュ値は何ですか?また23です。異なるべき乗を使用していますが、それらの基底は同じですが、前の係数を使用して指数の違いを埋め合わせることができます異なるオブジェクトのハッシュ結果が同じであるこの種の状況は、ハッシュ衝突と呼ばれます。これは、私たちの期待に沿わないものです。しかし、確かなことは、ほとんどの場合、ハッシュの衝突はほとんど避けられないということです。私たちのハッシュの目的は、写真を撮るように、元の複雑で巨大なデータを単純な数値または文字列に置き換えることです。2人の人物も非常に似ている可能性があります。

このプロセスは、多くの情報圧縮や情報損失が発生します。たとえば、元の2000ビットデータを表すために10ビットの数値を使用します。どんな戦略を使っても、10ビットで表現できるデータは限られています。ピジョンケージの原則によると、ハッシュに十分なデータがある限り、2つの異なるデータハッシュの結果の間には常に衝突が生じます。

ただし、適切な要素とアルゴリズムを設計することにより、衝突の確率を低減または制御できます。たとえば、この問題では、選択する素数大きいほど、衝突する可能性が低くなります。選択される素数大きいほど、衝突を形成するために繰り返す必要がある文字が多くなり、確率が低くなります。

最後に、完全なコードを見てみましょう。

from collections import defaultdict
import math


class Solution:
    def splitStr(self, s):
        hashtable = [0 for _ in range(30)]
        ret = 0
        for c in s:
            hashtable[ord(c) - ord('a')] += 1
            
        # hash算法
        for i in range(26):
            ret = ret * 23 + hashtable[i]
            # 控制返回值在32个bit内
            ret %= (1 << 32)
        return ret
    
    def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
        groups = defaultdict(list)
        for s in strs:
            key = self.splitStr(s)
            groups[key].append(s)
        
            
        ret = []
        for _, v in groups.items():
            ret.append(v)
        return ret
        

コードに詳細がありますv * pow(23、k)と書いたところですが、なぜここで文言を変更したのですか?

Pythonのpow関数は浮動小数点数を返すため、精度が失われ、ハッシュの衝突の確率が大幅に増加するため、pow関数に適用されないメソッドを変更しました。

まとめ

難易度に関しては、今日の問題は難しくありません。ハッシュアルゴリズムを知らない人でも解決策を考えることができます。しかし、私たちの目標は、質問を切り取ることではなく、切り取られた質問から成長することです。ハッシュアルゴリズムは、多くのアプリケーションやシナリオで使用される非常に重要で非常に基本的なアルゴリズムです。したがって、私たちは、将来の成長に非常に役立つハッシュアルゴリズムの原理を理解しています。

今日の記事はそれだけです。もし何かやりがいを感じた場合は、フォローまたは再投稿しください。あなたの努力は私にとって非常に重要です。

ここに画像の説明を挿入

117件の元の記事を公開 61のような 10,000以上の訪問

おすすめ

転載: blog.csdn.net/TechFlow/article/details/105497158
おすすめ