C語言HashTable基本理解

文章目錄

  • 一、哈希表概念
    • 1. 哈希表的基本概念
    • 2. 哈希表的核心組件
      • 2.1 哈希函數
      • 2.2 沖突處理(哈希碰撞)
    • 3.哈希表的三種結構
      • (1) 數組作為哈希表
        • 示例:
      • 2. Set(集合)
        • 示例:查找數組中的重復元素
          • 1. Set 基礎概念
          • 2. TypeScript 泛型約束
      • 3. Map(字典)
        • 示例:兩數之和
          • 1. Map 基礎概念
    • 4.哈希表性能分析
      • 1. 時間復雜度
      • 2. 影響性能的關鍵因素
        • (1)哈希函數的質量
        • (2)沖突處理方式
        • (3)負載因子(Load Factor)
      • 3. 性能優化策略
        • (1)選擇高效的哈希函數
        • (2)優化沖突處理
        • (3)動態擴容
      • 4. 實際應用中的性能對比
    • 5.與其他數據結構對比
      • 1. 時間復雜度對比
      • 2. 空間復雜度對比
      • 3. 特性與適用場景對比
      • 4. 典型應用場景
      • 5. 選擇建議
    • 6.哈希表的內存管理
      • 1. 基本內存結構
      • 2. 內存分配策略
        • (1)靜態分配
        • (2)動態分配
      • 3. 沖突處理與內存開銷
        • (1)鏈地址法
      • 4. 動態擴容與內存重分配
        • (1)觸發條件
        • (2)擴容步驟
        • (3)內存開銷
      • 5. 內存優化策略
        • (1)預分配與分批擴容
        • (2)內存池技術
        • (3)壓縮哈希表
      • 6. 內存泄漏風險
      • 7. 不同語言的實現差異
      • 總結
  • 二、C 語言實現哈希表
  • 三、例題示例
    • (1)四數相加2
    • (2)隨機鏈表的復制
    • (3)有效的字母異位詞

一、哈希表概念

1. 哈希表的基本概念

? 哈希表(Hash Table),也稱為散列表,是一種根據鍵(Key)直接訪問存儲位置的數據結構。哈希表的核心構成部分有兩個:哈希函數和數組

? 它通過哈希函數(Hash Function)將鍵映射到一個固定大小數組的索引上,這個數組被稱為哈希表,存儲在該索引位置的數據被稱為值(Value)。我們能把鍵(Key)轉換為數組的索引,進而快速地進行數據的插入、查找和刪除操作。

2. 哈希表的核心組件

2.1 哈希函數

? 哈希函數是哈希表的核心,它的作用是將鍵轉換為哈希表中的一個索引。一個好的哈希函數應該滿足以下幾個條件:

  • 確定性:對于相同的鍵,哈希函數必須始終返回相同的索引。
  • 均勻性:哈希函數應該盡可能均勻地將鍵分布到哈希表的各個位置,以減少沖突的發生。
  • 高效性:哈希函數的計算應該盡可能快,以保證哈希表的操作效率。

常見的哈希函數有取模法,直接定址法,除留余數法,平方取中法。

  • 取模法:

index = key % table_size,其中 key 是鍵,table_size 是哈希表的大小。

  • 直接定址法:一種將鍵值直接作為哈希地址的方法。具體來說,它通過一個線性變換來確定哈希地址。

H(key) = a·key + b

假設要統計不同年齡的人數,年齡范圍是 0-100,那么可以直接用年齡作為索引,即:

int count[101] = {0};  // 索引0-100對應年齡0-100
  • 除留余數法:一種較為常用的哈希函數構造方法,它通過取鍵值除以哈希表大小的余數來得到哈希地址。

H(key) = key % m

若哈希表的大小 m 為 10,對于鍵值 23,其哈希地址為:

H(23) = 23 % 10 = 3

若 m 是 2 的冪次方,那么哈希地址就相當于鍵值的低位部分,這可能會導致更多的沖突。

  • 平方取中法:先計算鍵值的平方,然后取中間的幾位作為哈希地址。

步驟

  1. 計算鍵值的平方,即key2
  2. 取平方結果中間的幾位數字作為哈希地址。

示例:假設哈希表的大小為 1000(即需要 3 位哈希地址):

  • 若鍵值為 25,252 = 625,那么哈希地址就是 625。
  • 若鍵值為 400,4002 = 160000,取中間三位為 000,所以哈希地址是 0。
方法優點缺點適用場景
直接定址法無沖突,簡單直接可能浪費大量空間鍵分布連續且密集
除留余數法實現簡單,應用廣泛m 選擇不當易產生沖突大多數場景
平方取中法分布均勻,對鍵的每一位都敏感計算相對復雜鍵的每一位都有影響的場景
取模法空間利用率為 100%,鍵值任意擴容時,m 值改變,所有鍵需要重新計算哈希地址鍵的分布較為隨機且均勻,哈希表大小固定且不需要動態擴容

2.2 沖突處理(哈希碰撞)

在這里插入圖片描述

? 如圖所示:當目標數組經過Hash FunctionKey轉化為Hash TableIndex時,有可能發生重復利用一個Index的情況,若不采取操作,可能會覆蓋原來存儲的數據,我最常用的是采用鏈表的方法,即在Hash Table每個Index所指的地方維護一個鏈表,將具有相同Index的數據存放到鏈表中。

? 由于哈希函數的映射是多對一的,不同的鍵可能會映射到相同的索引,這種情況稱為沖突。常見的沖突處理方法有以下兩種:

  • 開放地址法:當發生沖突時,通過某種探測方法(如線性探測、二次探測等)尋找下一個空閑的位置來存儲數據。
  • 鏈地址法:在哈希表的每個位置上維護一個鏈表(或其他數據結構),當發生沖突時,將具有相同哈希值的數據存儲在該位置的鏈表中。

3.哈希表的三種結構

哈希結構(Hash Structures)是計算機科學中非常重要的數據結構,主要用于高效地存儲和查找數據。以下是三種最常見的哈希結構及其詳細介紹和示例。

(1) 數組作為哈希表

特點

  • 適用于鍵的范圍已知且較小的情況
  • 直接使用數組索引作為鍵
  • 訪問時間復雜度:O(1)

適用場景

  • 字符統計(如小寫字母統計)
  • 固定范圍的數字統計
示例:
    const count: number[] = new Array(26).fill(0);const aCode: number = 'a'.charCodeAt(0);

2. Set(集合)

特點

  • 存儲唯一值
  • 快速判斷元素是否存在
  • 添加、刪除、查找時間復雜度:O(1)

適用場景

  • 去重
  • 快速查找元素是否存在
示例:查找數組中的重復元素
const numSet: Set<number> = new Set<number>();

在 TypeScript 中,const numSet = new Set<number>(); 創建了一個強類型的集合(Set),它只能存儲 數值類型(number) 的元素。下面詳細解釋其功能、用法和注意事項:

1. Set 基礎概念

Set 是 ES6 引入的一種數據結構,類似于數組,但具有以下特點:

  • 元素唯一:不允許重復值。
  • 無序性:元素沒有固定順序。
  • 高效查找:基于哈希表實現,插入、刪除、查找的時間復雜度接近 O (1)。
2. TypeScript 泛型約束

<number> 是泛型語法,用于約束 Set 中元素的類型。

3. Map(字典)

特點

  • 存儲鍵值對
  • 鍵可以是任意類型(對象引用需注意)
  • 添加、刪除、查找時間復雜度:O(1)

適用場景

  • 需要保存鍵值映射關系
  • 統計頻率
  • 緩存
示例:兩數之和
const numMap = new Map<number, number>();

在 TypeScript 中,const numMap = new Map<number, number>(); 創建了一個強類型的映射(Map),它只能存儲 鍵和值均為數值類型(number) 的鍵值對。下面詳細解釋其功能、用法和注意事項:

1. Map 基礎概念

Map 是 ES6 引入的一種數據結構,類似于對象(Object),但具有以下特點:

  • 鍵的類型靈活:鍵可以是任意類型(對象、函數、基本類型等),而對象的鍵只能是字符串或 Symbol。
  • 有序性:按插入順序存儲鍵值對,遍歷時順序與插入順序一致。
  • 高效查找:基于哈希表實現,插入、刪除、查找的時間復雜度接近 O (1)。

4.哈希表性能分析

1. 時間復雜度

哈希表的核心操作(插入、查找、刪除)的時間復雜度在平均情況下為 O(1),但在最壞情況下可能退化為 O(n)。具體分析如下:

操作平均時間復雜度最壞時間復雜度說明
插入(Insert)O(1)O(n)需計算哈希值并處理沖突(如鏈表遍歷)。
查找(Search)O(1)O(n)需計算哈希值并遍歷可能存在的沖突鏈表。
刪除(Delete)O(1)O(n)需先查找元素,再從鏈表或數組中刪除。

最壞情況發生在所有鍵都映射到同一哈希地址時。

2. 影響性能的關鍵因素

(1)哈希函數的質量
  • 均勻性:好的哈希函數應使鍵均勻分布在哈希表中,減少沖突。
  • 計算效率:哈希函數本身的計算復雜度應盡量低(如取模法)。
(2)沖突處理方式
  • 鏈地址法:每個槽位維護一個鏈表,沖突元素依次插入鏈表。
    • 優點:實現簡單,適用于沖突頻繁的場景。
    • 缺點:鏈表過長時查詢效率下降,需額外指針空間。
  • 開放尋址法:沖突時通過探測序列(如線性探測、二次探測)尋找下一個空位。
    • 優點:無需額外指針,空間利用率高。
    • 缺點:易產生聚集現象,導致后續沖突概率增加。
(3)負載因子(Load Factor)

負載因子定義為:

α = 鍵的數量 / 哈希表大小
  • α 越小:沖突概率越低,性能越好,但空間利用率低。
  • α 越大:沖突概率越高,性能下降,需擴容以維持效率。

經驗法則

  • 鏈地址法:通常將 α 控制在 0.75~1.0 之間。
  • 開放尋址法:α 應控制在 0.5 以下,避免聚集。

3. 性能優化策略

(1)選擇高效的哈希函數
  • 除留余數法H(key) = key % m,其中 m 取質數以減少沖突。
  • 字符串哈希:使用多項式哈希(如 DJB2 算法):

DJB2 算法是一種由 Daniel J. Bernstein 設計的字符串哈希函數,其核心思想是通過移位和加法操作將字符串轉換為一個無符號整數哈希值。

hash(i) = hash(i-1) * 33 + str[i]

其中:

  • hash(i) 是前 i 個字符的哈希值。
  • str[i] 是字符串的第 i 個字符(ASCII 值)。
  • 初始值hash(0) = 5381
unsigned int hash(char* str) {unsigned int hash = 5381;//研究表明,這個值能減少哈希沖突的概率。//33 是一個奇質數,實驗證明它在哈希函數中表現良好,能有效減少沖突。while (*str) hash = ((hash << 5) + hash) + (*str++); return hash;
}
(2)優化沖突處理
  • 鏈地址法改進:
    • 使用紅黑樹代替鏈表,將最壞時間復雜度從 O (n) 降至 O (log n)(如 Java 的 HashMap 在鏈表長度超過 8 時轉換為紅黑樹)。
(3)動態擴容

當負載因子超過閾值時,擴容并重新哈希:

  • 步驟:
    1. 創建更大的新哈希表(通常擴容為原大小的 2 倍)。
    2. 遍歷原哈希表,將所有鍵重新計算哈希值并插入新表。

4. 實際應用中的性能對比

場景鏈地址法哈希表紅黑樹
平均查找時間O(1)O(log n)
最壞查找時間O(n)O(log n)
空間利用率中等(需額外指針)低(需左右子樹指針)
適用負載因子α ≤ 1.0無限制
動態擴容開銷較高(需重新哈希)低(支持動態調整)

結論

  • 哈希表在平均情況下性能遠優于樹結構(如紅黑樹)。
  • 當鍵數量不確定或沖突頻繁時,鏈地址法更靈活。

5.與其他數據結構對比

1. 時間復雜度對比

數據結構插入(平均)插入(最壞)查找(平均)查找(最壞)刪除(平均)刪除(最壞)
哈希表O(1)O(n)O(1)O(n)O(1)O(n)
數組O(1)O(n)O(1)O(1)O(n)O(n)
鏈表O(1)O(1)O(n)O(n)O(n)O(n)
二叉搜索樹O(log n)O(n)O(log n)O(n)O(log n)O(n)
紅黑樹O(log n)O(log n)O(log n)O(log n)O(log n)O(log n)

說明

  • 哈希表的最壞情況發生在所有鍵沖突時(如哈希函數設計不當)。
  • 數組的插入 / 刪除最壞情況發生在需要擴容或移動元素時。
  • 二叉搜索樹的最壞情況發生在樹退化為鏈表時(如插入有序數據)。

2. 空間復雜度對比

數據結構空間復雜度額外開銷
哈希表O(n)哈希表數組、鏈表指針
數組O(n)連續內存塊,可能浪費空間
鏈表O(n)每個節點的指針
二叉搜索樹O(n)每個節點的左右子樹指針
紅黑樹O(n)每個節點的顏色位、指針

說明

  • 哈希表的空間利用率取決于負載因子(通常為 0.75~1.0)。
  • 紅黑樹的每個節點額外存儲一個顏色位(1 bit),用于維持平衡。

3. 特性與適用場景對比

數據結構有序性動態擴容優勢場景劣勢場景
哈希表無序支持快速查找、插入、刪除(平均 O (1)),適合鍵值對存儲(如緩存、數據庫索引)不支持有序遍歷,哈希函數設計不當會導致性能下降
數組有序需重建隨機訪問(O (1)),元素類型一致且數量固定插入 / 刪除效率低,擴容成本高
鏈表有序動態頻繁插入 / 刪除(O (1)),元素數量不確定隨機訪問效率低(O (n))
二叉搜索樹有序動態有序遍歷(O (n)),支持范圍查詢最壞情況退化為鏈表(O (n)),需維護平衡性
紅黑樹有序動態高效的插入、刪除、查找(O (log n)),適合需頻繁修改的有序集合實現復雜,空間開銷略高

4. 典型應用場景

數據結構應用場景
哈希表緩存(如 Redis)、編程語言內置字典(如 Python 的 dict)、數據庫索引
數組科學計算(如 NumPy)、矩陣運算、需要隨機訪問的固定大小數據
鏈表內存管理(如操作系統的空閑內存塊管理)、LRU 緩存淘汰策略
二叉搜索樹文件系統目錄結構、簡單的排序與查找
紅黑樹C++ STL 中的 map/set、Java 的 TreeMap/TreeSet、Linux 內核進程調度

5. 選擇建議

  • 優先選哈希表:若需快速查找且無需有序遍歷(如緩存、去重)。
  • 優先選數組:若需隨機訪問且元素數量固定(如矩陣、向量)。
  • 優先選鏈表:若需頻繁插入 / 刪除且無需隨機訪問(如實現棧、隊列)。
  • 優先選紅黑樹:若需有序遍歷或范圍查詢(如數據庫索引、有序集合)。

6.哈希表的內存管理

1. 基本內存結構

哈希表通常由兩部分組成:

  • 哈希數組:存儲桶(Bucket)的固定大小數組,每個桶可指向一個鏈表或其他數據結構。
  • 鏈表 / 紅黑樹:處理哈希沖突時,每個桶可能鏈接多個元素。

示例
鏈地址法哈希表的內存布局:

在這里插入圖片描述

2. 內存分配策略

(1)靜態分配
  • 特點:哈希數組大小固定,初始化時分配內存。
  • 優點:實現簡單,無需動態擴容。
  • 缺點:可能浪費空間或導致沖突頻繁。

示例

#define SIZE 100
Node* table[SIZE];  // 靜態數組,大小為100
(2)動態分配
  • 特點:運行時根據需要調整哈希數組大小。
  • 優點:空間利用率高,減少沖突。
  • 缺點:擴容操作耗時(O (n))。

示例

int size = 100;
Node** table = (Node**)calloc(size, sizeof(Node*));  // 動態分配

3. 沖突處理與內存開銷

(1)鏈地址法
  • 每個桶維護鏈表:每個節點包含鍵值對和指向下一個節點的指針。
  • 內存開銷:
    • 哈希數組:O(m)(m 為桶數量)。
    • 鏈表節點:O(n)(n 為鍵值對數量),每個節點需額外指針。

優化
當鏈表過長時,可將鏈表轉換為紅黑樹(如 Java 的 HashMap),將最壞時間復雜度從 O (n) 降至 O (log n),但會增加每個節點的內存開銷(額外存儲顏色位和父節點指針)。

4. 動態擴容與內存重分配

(1)觸發條件

當負載因子(α = n/m)超過閾值(通常為 0.75)時,需擴容。

(2)擴容步驟
  1. 創建更大的新哈希數組(通常擴容為原大小的 2 倍)。
  2. 遍歷原哈希表,重新計算每個元素的哈希值并插入新數組。
  3. 釋放原哈希數組內存。

示例

void resize(HashTable* ht) {int new_size = next_prime(ht->size * 2);  // 擴容為原大小的2倍Node** new_table = (Node**)calloc(new_size, sizeof(Node*));// 重新哈希所有元素for (int i = 0; i < ht->size; i++) {Node* curr = ht->table[i];while (curr) {Node* next = curr->next;unsigned int idx = hash(curr->key, new_size);curr->next = new_table[idx];new_table[idx] = curr;curr = next;}}free(ht->table);  // 釋放原數組內存ht->table = new_table;ht->size = new_size;
}
(3)內存開銷
  • 擴容期間需額外 O(m) 空間存儲新數組。
  • 若元素數量波動較大,頻繁擴容會導致內存碎片。

5. 內存優化策略

(1)預分配與分批擴容
  • 預分配:預估元素數量,初始化時分配足夠空間。
  • 分批擴容:在高并發場景下,將擴容操作分攤到多次插入中,減少單次擴容的內存壓力。
(2)內存池技術
  • 預先分配大塊內存,按需分配給節點,減少頻繁 malloc/free 的開銷。
  • 降低內存碎片,提高內存利用率。

示例

// 內存池結構
typedef struct MemoryPool {Node* free_list;size_t block_size;
} MemoryPool;// 從內存池分配節點
Node* pool_alloc(MemoryPool* pool) {if (!pool->free_list) {// 分配新塊Node* block = (Node*)malloc(pool->block_size * sizeof(Node));// 將新塊中的節點連接到空閑列表for (int i = 0; i < pool->block_size - 1; i++) {block[i].next = &block[i + 1];}block[pool->block_size - 1].next = NULL;pool->free_list = block;}Node* node = pool->free_list;pool->free_list = node->next;return node;
}
(3)壓縮哈希表
  • 對于稀疏哈希表(負載因子極低),可使用壓縮技術減少空間浪費。
  • 例如,使用 BitArray 標記已使用的桶,僅存儲有效元素。

6. 內存泄漏風險

  • 未釋放鏈表節點:刪除元素時,若未正確釋放鏈表節點內存,會導致泄漏。
  • 循環引用:在雙向鏈表或樹結構中,若存在循環引用,垃圾回收機制無法回收內存。

正確釋放示例

void free_hash_table(HashTable* ht) {for (int i = 0; i < ht->size; i++) {Node* curr = ht->table[i];while (curr) {Node* temp = curr;curr = curr->next;free(temp->key);  // 釋放鍵內存free(temp);       // 釋放節點內存}}free(ht->table);  // 釋放哈希數組free(ht);         // 釋放哈希表結構
}

7. 不同語言的實現差異

語言內存管理方式特點
C手動管理(malloc/free)需自行處理內存分配、釋放和擴容,靈活性高但易出錯
Java自動垃圾回收(GC)無需手動釋放內存,但擴容時可能觸發 GC,影響性能
Python引用計數 + GC字典底層使用哈希表,內存由解釋器自動管理
Go自動垃圾回收 + 內存池標準庫 map 使用哈希表,并發場景下通過內存池優化

總結

哈希表的內存管理需平衡空間利用率和性能:

  • 鏈地址法:空間開銷較大,但實現簡單,適合沖突頻繁的場景。
  • 開放尋址法:空間利用率高,但需嚴格控制負載因子,避免聚集。
  • 動態擴容:需權衡擴容時機,避免頻繁擴容導致的性能下降和內存碎片。
  • 內存池和壓縮技術:可進一步優化內存使用,但會增加實現復雜度。

二、C 語言實現哈希表

// 定義哈希表節點結構
typedef struct HashNode {int key;int value;struct HashNode* next;
} HashNode;// 定義哈希表結構
typedef struct HashTable {int size;HashNode** table;
} HashTable;
  1. 哈希表節點(HashNode
    • 包含三個字段:key(鍵)、value(值)和next(指向下一個節點的指針)。
    • 通過鏈表結構處理哈希沖突(不同的鍵映射到相同索引時)。
  2. 哈希表(HashTable
    • 包含兩個字段:size(哈希表大小)和table(指向指針數組的指針)。
    • table數組的每個元素是一個鏈表頭指針
// 初始化哈希表
HashTable* createHashTable(int size) {HashTable* hashTable = (HashTable*)malloc(sizeof(HashTable));hashTable->size = size;hashTable->table = (HashNode**)malloc(size * sizeof(HashNode*));for (int i = 0; i < size; i++) {hashTable->table[i] = NULL;}return hashTable;
}// 創建新的哈希表節點
HashNode* createHashNode(int key, int value) {HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));newNode->key = key;newNode->value = value;newNode->next = NULL;return newNode;
}
  1. 分配哈希表結構內存
    • 使用 malloc 動態分配 HashTable 結構體的內存空間。
  2. 設置哈希表大小
    • 將傳入的 size 參數賦值給哈希表的 size 字段,決定哈希表的桶bucket數量。
  3. 分配哈希表數組內存
    • 分配一個指針數組,每個元素是一個指向 HashNode 的指針(即鏈表頭指針)。
  4. 初始化所有桶為空
// 哈希函數
int hashFunction(int key, int size) {return key % size;
}// 插入節點到哈希表
void insert(HashTable* hashTable, int key, int value) {int index = hashFunction(key, hashTable->size);HashNode* newNode = createHashNode(key, value);if (hashTable->table[index] == NULL) {hashTable->table[index] = newNode;} else {HashNode* current = hashTable->table[index];while (current->next != NULL) {current = current->next;}current->next = newNode;}
}// 在哈希表中查找節點
int search(HashTable* hashTable, int key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];while (current != NULL) {if (current->key == key) {return current->value;}current = current->next;}return -1;
}// 從哈希表中刪除節點
void deleteNode(HashTable* hashTable, int key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];HashNode* prev = NULL;while (current != NULL && current->key != key) {prev = current;current = current->next;}if (current == NULL) {return;}if (prev == NULL) {hashTable->table[index] = current->next;} else {prev->next = current->next;}free(current);
}
  1. 哈希函數
    使用取模運算將鍵映射到數組索引。
    • 作用:決定鍵值對應該存儲在哈希表的哪個 “桶” 中。
  2. 插入操作
    • 通過哈希函數計算索引。
    • 如果對應索引為空,直接插入。如果不為空,遍歷鏈表將新節點添加到尾部(尾插法)。
  3. 查找操作
    • 通過哈希函數定位索引。
    • 遍歷對應鏈表,比較鍵值。找到則返回值,未找到返回 - 1
  4. 刪除操作
    • 通過哈希函數定位索引
    • 遍歷鏈表查找目標節點,調整前驅節點指針跳過目標節點,釋放目標節點內存。
// 釋放哈希表內存
void freeHashTable(HashTable* hashTable) {for (int i = 0; i < hashTable->size; i++) {HashNode* current = hashTable->table[i];while (current != NULL) {HashNode* temp = current;current = current->next;free(temp);}}free(hashTable->table);free(hashTable);
}
  1. 遍歷哈希表數組
    • 獲取當前桶的鏈表頭指針,若鏈表不為空,則釋放鏈表中的所有節點。
  2. 釋放鏈表節點
    • 保存當前節點指針到臨時變量,移動到下一個節點,釋放臨時保存的節點內存,重復直到鏈表末尾。
  3. 釋放哈希表結構
    • 釋放哈希表的數組(存儲鏈表頭指針的數組)。
    • 釋放哈希表本身的結構體。

三、例題示例

(1)四數相加2

454. 四數相加 II

給你四個整數數組 nums1nums2nums3nums4 ,數組長度都是 n ,請你計算有多少個元組 (i, j, k, l) 能滿足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 1:

輸入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
輸出:2
解釋:
兩個元組如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

示例 2:

輸入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
輸出:1
  1. 定義哈希表節點和哈希表結構體
typedef struct HashNode {int key;int count;struct HashNode* next;
} HashNode;typedef struct HashTable {int size;HashNode** table;
} HashTable;

? HashNode 結構體表示哈希表中的一個節點,包含一個鍵 key、一個計數 count(用于記錄該鍵出現的次數)以及指向下一個節點的指針 nextHashTable 結構體表示整個哈希表,包含哈希表的大小 size 和一個指向 HashNode 指針數組的指針 table,用于存儲哈希表的各個桶。

  1. 創建哈希表函數 createHashTable
HashTable* createHashTable(int size) {HashTable* hashTable = (HashTable*)malloc(sizeof(HashTable));hashTable->size = size;hashTable->table = (HashNode**)malloc(size * sizeof(HashNode*));for (int i = 0; i < size; i++) {hashTable->table[i] = NULL;}return hashTable;
}

? 該函數動態分配一個 HashTable 結構體的內存,并設置其大小 size。然后,為哈希表的桶數組 table 分配內存,并將每個桶的指針初始化為 NULL,最后返回創建好的哈希表指針。

  1. 哈希函數 hashFunction
int hashFunction(int key, int size) { return (key % size + size) % size; }

? 該函數接受一個鍵 key 和哈希表的大小 size,通過取模運算將鍵映射到哈希表的索引位置。(key % size + size) % size 確保了即使 key % size 為負數,也能得到一個有效的非負索引。

  1. 插入節點到哈希表函數 insert
void insert(HashTable* hashTable, int key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];while (current != NULL) {if (current->key == key) {current->count++;return;}current = current->next;}HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));newNode->key = key;newNode->count = 1;newNode->next = hashTable->table[index];hashTable->table[index] = newNode;
}

? 該函數首先計算鍵 key 的哈希索引 index。然后,遍歷哈希表中該索引位置的鏈表,如果找到相同的鍵,則將其計數 count 加 1 并返回。如果沒有找到相同的鍵,則創建一個新的 HashNode,將其計數初始化為 1,并將其插入到鏈表的頭部。

  1. 獲取鍵的計數函數 getCount
int getCount(HashTable* hashTable, int key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];while (current != NULL) {if (current->key == key) {return current->count;}current = current->next;}return 0;
}

? 該函數計算鍵 key 的哈希索引 index,然后遍歷該索引位置的鏈表,查找鍵 key。如果找到,則返回其計數 count;如果沒有找到,則返回 0。

  1. 釋放哈希表內存函數 freeHashTable
void freeHashTable(HashTable* hashTable) {for (int i = 0; i < hashTable->size; i++) {HashNode* current = hashTable->table[i];while (current != NULL) {HashNode* temp = current;current = current->next;free(temp);}}free(hashTable->table);free(hashTable);
}

? 該函數遍歷哈希表的每個桶,釋放鏈表中的每個節點的內存,然后釋放桶數組的內存,最后釋放哈希表結構體的內存。

  1. 四數之和計數函數 fourSumCount
int fourSumCount(int* nums1, int nums1Size, int* nums2, int nums2Size,int* nums3, int nums3Size, int* nums4, int nums4Size) {HashTable* hashTable = createHashTable(400);int result = 0;for (int i = 0; i < nums1Size; i++) {for (int j = 0; j < nums2Size; j++) {int sum12 = nums1[i] + nums2[j];insert(hashTable, sum12);}}for (int k = 0; k < nums3Size; k++) {for (int l = 0; l < nums4Size; l++) {int sum34 = nums3[k] + nums4[l];result += getCount(hashTable, -sum34);}}freeHashTable(hashTable);return result;
}

? 首先,創建一個哈希表 hashTable。然后,遍歷數組 nums1nums2,計算它們元素的和 sum12,并將其插入到哈希表中,同時記錄出現的次數。接著,遍歷數組 nums3nums4,計算它們元素的和 sum34,并在哈希表中查找 -sum34 的計數,將計數累加到 result 中。最后,釋放哈希表的內存并返回 result

(2)隨機鏈表的復制

138. 隨機鏈表的復制

給你一個長度為 n 的鏈表,每個節點包含一個額外增加的隨機指針 random ,該指針可以指向鏈表中的任何節點或空節點。

構造這個鏈表的 深拷貝。 深拷貝應該正好由 n全新 節點組成,其中每個新節點的值都設為其對應的原節點的值。新節點的 next 指針和 random 指針也都應指向復制鏈表中的新節點,并使原鏈表和復制鏈表中的這些指針能夠表示相同的鏈表狀態。復制鏈表中的指針都不應指向原鏈表中的節點

例如,如果原鏈表中有 XY 兩個節點,其中 X.random --> Y 。那么在復制鏈表中對應的兩個節點 xy ,同樣有 x.random --> y

返回復制鏈表的頭節點。

用一個由 n 個節點組成的鏈表來表示輸入/輸出中的鏈表。每個節點用一個 [val, random_index] 表示:

  • val:一個表示 Node.val 的整數。
  • random_index:隨機指針指向的節點索引(范圍從 0n-1);如果不指向任何節點,則為 null

你的代碼 接受原鏈表的頭節點 head 作為傳入參數。

  1. 定義數據結構
typedef struct HashNode {struct Node* key;struct Node* value;struct HashNode* next;
} HashNode;typedef struct HashTable {int size;HashNode** table;
} HashTable;
  • 首先定義了鏈表節點 Node 的結構體,包含值 val、指向下一個節點的指針 next 和指向隨機節點的指針 random

  • 接著定義了哈希表節點 HashNode 的結構體,包含鍵 key(指向原鏈表節點的指針)、值 value(指向復制鏈表節點的指針)和指向下一個哈希表節點的指針 next

  • 然后定義了哈希表 HashTable 的結構體,包含哈希表的大小 size 和一個指向 HashNode 指針數組的指針 table

  1. 創建鏈表節點函數 creatNode
struct Node* creatNode(int val) {struct Node* newNode = (struct Node*)malloc(sizeof(struct Node));newNode->val = val;newNode->next = NULL;newNode->random = NULL;return newNode;
}

該函數動態分配一個 Node 結構體的內存,并初始化其值 val,同時將 nextrandom 指針初始化為 NULL,最后返回創建好的鏈表節點指針。

  1. 創建哈希表節點函數 createHashNode
HashNode* createHashNode(struct Node* key, struct Node* value) {HashNode* newNode = (HashNode*)malloc(sizeof(HashNode));newNode->key = key;newNode->value = value;newNode->next = NULL;return newNode;
}

該函數動態分配一個 HashNode 結構體的內存,并初始化其鍵 key 和值 value,同時將 next 指針初始化為 NULL,最后返回創建好的哈希表節點指針。

  1. 創建哈希表函數 createHashTable
HashTable* createHashTable(int size) {HashTable* hashTable = (HashTable*)malloc(sizeof(HashTable));hashTable->size = size;hashTable->table = (HashNode**)malloc(size * sizeof(HashNode*));for (int i = 0; i < size; i++) {hashTable->table[i] = NULL;}return hashTable;
}

該函數動態分配一個 HashTable 結構體的內存,并設置其大小 size。然后,為哈希表的桶數組 table 分配內存,并將每個桶的指針初始化為 NULL,最后返回創建好的哈希表指針。

  1. 哈希函數 hashFunction
int hashFunction(struct Node* key, int size) {return ((unsigned long)key) % size;
}

該函數接受一個指向鏈表節點的指針 key 和哈希表的大小 size,通過將指針轉換為無符號長整型并取模運算,將鍵映射到哈希表的索引位置。

在 C 語言中:

  • 指針本質:指針是一個內存地址,通常表示為無符號整數。
  • 類型安全:直接對指針進行算術運算(如取模)會導致類型錯誤,因此需要將指針轉換為整數類型。
  • 平臺兼容性unsigned long 類型通常足夠大,可以容納指針的完整值(在 32 位和 64 位系統中均適用)。

通過 (unsigned long)key,將指針值轉換為無符號長整型,確保其作為整數參與后續的取模運算。

  1. 插入節點到哈希表函數 insert
void insert(HashTable* hashTable, struct Node* key, struct Node* value) {int index = hashFunction(key, hashTable->size);HashNode* newNode = createHashNode(key, value);if (hashTable->table[index] == NULL) {hashTable->table[index] = newNode;} else {HashNode* current = hashTable->table[index];while (current->next != NULL) {current = current->next;}current->next = newNode;}
}

該函數首先計算鍵 key 的哈希索引 index。然后,創建一個新的 HashNode,并將其插入到哈希表中該索引位置的鏈表中。如果該索引位置的鏈表為空,則直接插入;否則,遍歷鏈表并將新節點插入到鏈表的末尾。

  1. 在哈希表中查找節點函數 search
struct Node* search(HashTable* hashTable, struct Node* key) {int index = hashFunction(key, hashTable->size);HashNode* current = hashTable->table[index];while (current != NULL) {if (current->key == key) {return current->value;}current = current->next;}return NULL;
}

該函數計算鍵 key 的哈希索引 index,然后遍歷該索引位置的鏈表,查找鍵 key。如果找到,則返回其對應的值 value;如果沒有找到,則返回 NULL

  1. 釋放哈希表內存函數 freeHashTable
void freeHashTable(HashTable* hashTable) {for (int i = 0; i < hashTable->size; i++) {HashNode* current = hashTable->table[i];while (current != NULL) {HashNode* temp = current;current = current->next;free(temp);}}free(hashTable->table);free(hashTable);
}

該函數遍歷哈希表的每個桶,釋放鏈表中的每個節點的內存,然后釋放桶數組的內存,最后釋放哈希表結構體的內存。

  1. 復制隨機鏈表函數 copyRandomList
struct Node* copyRandomList(struct Node* head) {if (head == NULL)return NULL;HashTable* hashTable = createHashTable(300);struct Node* cur = head;while (cur != NULL) {struct Node* newNode = creatNode(cur->val);insert(hashTable, cur, newNode);cur = cur->next;}cur = head;while (cur != NULL) {struct Node* newNode = search(hashTable, cur);if (cur->next != NULL) {newNode->next = search(hashTable, cur->next);}if (cur->random != NULL) {newNode->random = search(hashTable, cur->random);}cur = cur->next;}struct Node* new = search(hashTable, head);freeHashTable(hashTable);return new;
}

? 首先,創建一個哈希表 hashTable。然后,遍歷原鏈表,為每個節點創建一個復制節點,并將原節點和復制節點的對應關系插入到哈希表中。接著,再次遍歷原鏈表,通過哈希表找到復制節點,并設置其 nextrandom 指針。最后,釋放哈希表的內存,并返回復制鏈表的頭節點。

(3)有效的字母異位詞

有效的字母異位詞

給定兩個字符串 st ,編寫一個函數來判斷 t 是否是 s 的 字母異位詞。

示例 1:

輸入: s = "anagram", t = "nagaram"
輸出: true

示例 2:

輸入: s = "rat", t = "car"
輸出: false
  1. 檢查字符串長度
if (strlen(s) != strlen(t))return false;

如果兩個字符串的長度不相等,那么它們肯定不是字母異位詞,直接返回 false

  1. 創建哈希表
int* hashTable = (int*)malloc(sizeof(int) * 26);
for (int i = 0; i < 26; i++) {hashTable[i] = 0;
}

動態分配一個大小為 26 的整型數組 hashTable,用于記錄每個字符出現的次數。因為英文字母有 26 個,所以數組大小為 26。然后將數組的每個元素初始化為 0。

  1. 統計字符串 s 中字符的出現次數
for (int i = 0; i < strlen(s); i++) {hashTable[(s[i] - 'a')] += 1;
}

遍歷字符串 s,對于每個字符 s[i],計算其在 hashTable 中的索引位置 s[i] - 'a'(假設字符都是小寫英文字母),并將對應位置的計數器加 1。

  1. 檢查字符串 t 中字符的出現次數
for (int i = 0; i < strlen(t); i++) {if (hashTable[(t[i] - 'a')] == 0) {return false;} else {hashTable[(t[i] - 'a')] -= 1;}
}

遍歷字符串 t,對于每個字符 t[i],計算其在 hashTable 中的索引位置 t[i] - 'a'。如果對應位置的計數器為 0,說明字符串 t 中出現了字符串 s 中沒有的字符,那么它們不是字母異位詞,返回 false。否則,將對應位置的計數器減 1。

  1. 檢查哈希表中所有計數器是否為 0
for (int i = 0; i < 26; i++) {if (hashTable[i] != 0) {return false;}
}

遍歷哈希表 hashTable,如果有任何一個計數器不為 0,說明兩個字符串中字符的出現次數不相等,它們不是字母異位詞,返回 false

  1. 返回結果
return true;

如果上述所有檢查都通過,說明兩個字符串包含相同的字符且出現次數相同,即它們是字母異位詞,返回 true

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/77386.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/77386.shtml
英文地址,請注明出處:http://en.pswp.cn/web/77386.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【緩存與數據庫結合最終方案】偽從技術

實現偽從技術&#xff1a;基于Binlog的Following表變更監聽與緩存更新 技術方案概述 要實現一個專門消費者服務作為Following表的偽從&#xff0c;訂閱binlog并在數據變更時更新緩存&#xff0c;可以采用以下技術方案&#xff1a; 主要組件 MySQL Binlog監聽&#xff1a;使…

《100天精通Python——基礎篇 2025 第3天:變量與數據類型全面解析,掌握Python核心語法》

目錄 一、Python變量的定義和使用二、Python整數類型&#xff08;int&#xff09;詳解三、Python小數/浮點數&#xff08;float&#xff09;類型詳解四、Python復數類型(complex)詳解---了解五、Python字符串詳解(包含長字符串和原始字符串)5.1 處理字符串中的引號5.2 字符串的…

【前后端分離項目】Vue+Springboot+MySQL

文章目錄 1.安裝 Node.js2.配置 Node.js 環境3.安裝 Node.js 國內鏡像4.創建 Vue 項目5.運行 Vue 項目6.訪問 Vue 項目7.創建 Spring Boot 項目8.運行 Spring Boot 項目9.訪問 Spring Boot 項目10.實現 Vue 與 Spring Boot 聯動11.安裝 axios12.編寫請求13.調用函數請求接口14.…

線性代數(一些別的應該關注的點)

一、矩陣 矩陣運算&#xff1a;線性變換 縮放、平移、旋轉 無所不能的矩陣 - 三維圖形變換_嗶哩嗶哩_bilibili

01Redis快速入門(nosql、安裝redis、客戶端、命令及類型、java客戶端、序列化)

Redis的常見命令和客戶端使用 1.初識Redis Redis是一種鍵值型的NoSql數據庫&#xff0c;這里有兩個關鍵字&#xff1a; 鍵值型 NoSql 其中鍵值型&#xff0c;是指Redis中存儲的數據都是以key、value對的形式存儲&#xff0c;而value的形式多種多樣&#xff0c;可以是字符串…

AI編程:[體驗]從 0 到 1 開發一個項目的初體驗

一、開發信息 開發時間&#xff1a;1.5-2天工具使用&#xff1a; 不熟練&#xff0c;開發本項目前1天&#xff0c;才簡單使用了Cursor的功能 功能復雜度&#xff1a; 開發的功能相對簡單。頁面&#xff1a;2個&#xff0c;登錄頁面&#xff0c;個人中心頁面功能&#xff1a;5個…

LeetCode-392 判斷子序列

給定字符串 s 和 t &#xff0c;判斷 s 是否為 t 的子序列。 字符串的一個子序列是原始字符串刪除一些&#xff08;也可以不刪除&#xff09;字符而不改變剩余字符相對位置形成的新字符串。&#xff08;例如&#xff0c;"ace"是"abcde"的一個子序列&#…

Linux 系統監控大師:Glances 工具詳解助力自動化

看圖猜詩&#xff0c;你有任何想法都可以在評論區留言哦~ 摘要 Glances 是一款基于 Python 開發的跨平臺系統監控工具&#xff0c;集成了 CPU、內存、磁盤、網絡、進程等核心指標的實時監控能力&#xff0c;并支持命令行、Web界面、客戶端-服務器模式等多種使用場景。其輕量級…

Spring Boot 3.4.5 運行環境需求

&#x1f4dd; Spring Boot 3.4.5 運行環境要求 &#x1f33f; 1?? 基本需求 ?? JDK版本&#xff1a;最低 Java 17 &#x1f517; https://www.java.com/ 最高兼容至 Java 24 ?? 依賴框架&#xff1a;需搭配 Spring Framework 6.2.6 &#x1f517; https://docs.sprin…

在KEIL里C51和MDK兼容以及添加ARM compiler5 version編譯器

前言 我們想在一個keil里面可以打開32和51的文件&#xff0c;這樣就不需要兩個keil了 還有就是現在的keil&#xff0c;比如我用的是5.41的&#xff0c;就沒有5版本的處理器&#xff0c;所以要安裝 本篇文章我們來詳細講解如何實現上面說的兩個內容 準備的東西 1.ARM5編譯器 …

Flutter 彈窗隊列管理:支持優先級的線程安全通用彈窗隊列系統

在復雜的 Flutter 應用開發中&#xff0c;彈窗管理是一個常見難題。手動管理彈窗的顯示順序和條件判斷不僅繁瑣&#xff0c;還容易出錯。為此&#xff0c;我們實現了一個支持優先級的線程安全通用彈窗隊列管理系統。它能夠自動管理彈窗的顯示順序&#xff0c;支持條件判斷&…

鴻蒙NEXT開發剪貼板工具類(ArkTs)

import { pasteboard } from kit.BasicServicesKit; import { StrUtil } from ./StrUtil;/*** 剪貼板工具類* 需要權限&#xff1a;* ohos.permission.READ_PASTEBOARD // 允許應用讀取剪貼板。* author CSDN-鴻蒙布道師* since 2025/04/25*/ export class PasteboardUtil {…

FastAPI 零基礎入門指南:10 分鐘搭建高性能 API

一、為什么選擇 FastAPI&#xff1f; 想象一下&#xff0c;用 Python 寫 API 可以像搭積木一樣簡單&#xff0c;同時還能擁有媲美 Go 語言的性能&#xff0c;這個框架憑借三大核心優勢迅速風靡全球&#xff1a; 開發效率提升 3 倍&#xff1a;類型注解 自動文檔&#xff0c;…

【算法】BFS-解決FloodFill問題

目錄 FloodFill問題 圖像渲染 島嶼數量 島嶼的最大面積 被圍繞的區域 FloodFill問題 FloodFill就是洪水灌溉的意思&#xff0c;假設有下面的一塊田地&#xff0c;負數代表是凹地&#xff0c;正數代表是凸地&#xff0c;數字的大小表示凹或者凸的程度。現在下一場大雨&…

代碼隨想錄算法訓練營第三十七天|動態規劃part4

1049. 最后一塊石頭的重量 II 題目鏈接&#xff1a; 1049. 最后一塊石頭的重量 II - 力扣&#xff08;LeetCode&#xff09; 文章講解&#xff1a; 代碼隨想錄 思路&#xff1a; 理解為把石頭分成兩堆 使得兩堆的差值盡可能小 求這個最小值1 理解為往背包里裝物品 每個物品的…

(八)深入了解AVFoundation-采集:拍照功能的實現

引言 在上一篇文章中&#xff0c;我們初步完成了使用 AVFoundation 采集視頻數據的流程&#xff0c;掌握了 AVCaptureSession 的搭建與視頻流的預覽顯示。 本篇將繼續深入 AVFoundation&#xff0c;聚焦于靜態圖片采集的實現。通過 AVCapturePhotoOutput&#xff0c;我們可以…

git tag使用場景和實踐

背景 每次上線一個迭代&#xff0c;為了區分本次代碼的分支是哪個迭代的commit&#xff0c;可以給分支打上tag&#xff0c;這樣利于追蹤分支所屬迭代&#xff0c;如果devops沒有自動給分支打tag&#xff0c;需要自己來打 操作 1.查看當前tag git tag2.給分支打tag git tag…

從零開始掌握Linux數據流:管道與重定向完全指南

全文目錄 1 知識背景與核心概念1.1 操作系統的輸入輸出模型1.2 Shell 的中間人角色 2 重定向技術深度解析2.1 輸出重定向2.1.1 覆蓋寫2.1.2 追加寫2.1.3 錯誤重定向2.1.4 同時重定向 stdout 和 stderr 2.2 輸入重定向2.2.1 文件作為輸入源2.2.2 Here Document&#xff08;多行輸…

aws(學習筆記第三十九課) iot-core

文章目錄 aws(學習筆記第三十九課) iotcore(Internet Of Thing)學習內容:1. 整體架構1.1 代碼鏈接1.2 整體架構(概要)1.3 整體架構(詳細 )2. 代碼解析2.1 創建`IOT thing`2.2 創建`AWS IOT certificate`證書2.2.1 創建`lambda`需要的`role`2.2.2 創建`lambda`2.2.3 `lambd…

國家新政鼓勵游戲出海,全球化安全威脅如何解

本文作者&#xff1a;騰訊宙斯盾DDoS防護團隊 01 政策紅利釋放&#xff1a;游戲出海升級為“國家戰略工程” 01 4月21日&#xff0c;國務院新聞辦公室發布《加快推進服務業擴大開放綜合試點工作方案》&#xff0c;釋放了一個信號&#xff1a;首次將“游戲出海”列為戰略級工程&…