查詢演算法及雜湊表

PRO_Z發表於2022-03-06

1 二分查詢

1.1 重要概念

  • 擬解決的問題:判斷某個區間是否包含某個元素,無法確定區間中包含重複元素的具體位置;
  • 使用條件:查詢的區間必須符合單調性;
  • 本質:採用分治思想,將某個單調區間一分為二,保證留下的一半區間包含解,捨棄的一半區間不包含解;
  • 時間複雜度:\(O(log_2n)\)
  • 計算方式:二分查詢每查詢一次將原問題的規模n縮減到1/2,最糟糕的情況為n=1時,二分查詢獲得結果,此時二分查詢的次數為$ $$n/2^x=1\(,即\)x = log_2n$

1.2 應用場景

  • 判斷某個單調區間是否包含某個元素;
  • 前面一堆1,後面一堆0,如1111100000,查詢最後一個1出現的位置;(特殊情況1)
  • 前面一堆0,後面一堆1,如0000011111,查詢第一個1出現的位置;(特殊情況2)
查詢演算法及雜湊表 查詢演算法及雜湊表

1.3 程式碼演示

// 1 3 5 7 9 10
int binary_search(int *arr, int n ,int x) {
    int head = 0, tail = n - 1, mid;
    while (head <= tail) {
        mid = (head + tail) >> 1;
        if (arr[mid] == x) return mid;
        if (arr[mid] < x) head = mid + 1;
        else tail = mid - 1;
    }
    return -1;
}

// 1111100000
int binary_search1(int *arr, int n) {
    int head = -1, tail = n - 1, mid;   // 當查詢區間的元素全0時,為避免二義性,定義head=-1,而不是head=0
    while (head < tail) {
        mid = (head + tail + 1) >> 1;   // 上取整,否則會出現死迴圈
        if (arr[mid] == 1) head = mid;  // mid 有可能是最後一個1出現的位置
        else tail = mid -1;
    }
    // head = tail = -1 時,代表未找到,否則返回對應元素的位置
    return head;
}

// 00000111111
int binary_search2(int *arr, int n) {
    int head = 0, tail = n, mid;        // 當查詢區間的元素全0時,為避免二義性,定義tail=n,而不是tail=n-1
    while (head < tail) {
        mid = (head + tail) >> 1;
        if (arr[mid] == 1) tail = mid;  // mid 有可能是第一個1出現的位置
        else head = mid + 1;
    }
    // head = tail = n 時,代表未找到,否則返回對應元素的位置
    return head == n ? -1 : head;
}

2 三分查詢

2.1 擬解決的問題

二分查詢解決的是在單調序列中查詢目標值的問題,而三分查詢則是確定函式在凹/凸區間上的極值點;

2.2 演算法描述

    在函式f(x)的某個區間[l, r]上取2個分界點,其中m1位於l的1/3處,m2位於l的2/3處,即:$m1 = l + (r - l) / 3$,$m2 = r - (r - l) / 3$,這兩個點m1、m2把區間 [l, r] 分為3 個子區間。這裡以凸函式(即有最大值)為例討論:
  • 若f(m1) < f(m2),說明極值點位於[m1, r]區間內,可以不必再考慮[l, m1]區間;原因就是當f(m1) < f(m2)時,m1處於單調遞增區間段,故f(l) < f(m1);
  • 若f(m1) > f(m2),說明極值點位於[l, m2]區間內,可以不必再考慮[m2, r]區間;原因就是當f(m1) > f(m2)時,m2處於單調遞減區間段,故f(m2) > f(r);
  • 這樣,每一輪迭代都會把查詢範圍限制在原來的2/3,直到最終逼近極值點,即l和r之間的差值接近無窮小;
  • 三分查詢的時間複雜度:\(O(log_3n)\),二分查詢的時間複雜度:\(O(log_2n)\),儘管二者的複雜度級別一樣,但是二分查詢的效率更高,因為二分查詢是在1/2的區域尋找值,而三分查詢是在2/3的區域尋找值;
  • 參考部落格:https://www.jianshu.com/p/60d8c3e576d7https://zhuanlan.zhihu.com/p/257842997
查詢演算法及雜湊表

2.3 程式碼演示

double three_point_search(double (*func)(double), double l, double r) {
    double m1, m2;
    int flag = 0; // flag = 0:func 函式為凸函式;flag = 1:func 函式為凹函式
    // 凹凸函式判斷
    if (func((l + r) / 2.0) > (func(l) + func(r)) / 2.0 ) flag = 0;
    else flag = 1;
    #define EPSL 1e-6
    if (!flag) {
        while (fabs(r - l) > EPSL) {
            m1 = l + (r - l) / 3.0, m2 = r - (r - l) / 3.0;
            if (func(m1) < func(m2)) l = m1;
            else r = m2;
        }
    } else {
        while (fabs(r - l) > EPSL) {
            printf("l = %f, r = %f\n", l, r);
            m1 = l + (r - l) / 3.0, m2 = r - (r - l) / 3.0;
            if (func(m1) < func(m2)) r = m2;
            else l = m1;
        }
    }
    #undef EPSL
    return l;
}

3 雜湊表 HashTable

3.1 介紹

   基於陣列的“隨機訪問”特性,其可以通過下標快速地找到對應位置的元素,然而這種通過`<int>`下標尋找`<任意型別>`元素的對映關係並不通用(對映關係:`<int>`→`<任意型別>`)。所以,為了擁有陣列“快速訪問”的特性,以及在查詢元素過程中具有<`任意型別`>到`<任意型別>`的對映關係,雜湊表就應運而生了。雜湊表主要由**雜湊函式** 和 **雜湊衝突處理方法**兩部分組成,其中**雜湊函式**完成<`任意型別`>到`<int>`的對映(這兒用key-value來說明,key為輸入,value為輸出,即key到value的對映),但是這種對映關係並不唯一,即不同的key可能會有相同的value存在,若直接通過value來標記key的話,就會有覆蓋現象發生,此時就會用到 **雜湊衝突處理方法**。

   簡單來講,雜湊表(Hash table,也叫雜湊表),是通過雜湊函式將任意型別的資料轉化成一個整型數字,然後用這個整型數字對陣列長度取餘,將其結果作為陣列的下標,然後把這組資料儲存到對應下標的陣列空間(**資料儲存過程**)。在使用雜湊表進行資料查詢時,就是再次通過雜湊函式獲取陣列的下標,然後使用這個下標直接訪問對應位置的陣列元素,故雜湊表查詢資料的時間複雜度幾乎為O(1)(**資料查詢過程**)。
查詢演算法及雜湊表
  • 什麼是雜湊表(雜湊表)

    雜湊表就是通過雜湊函式將輸入對映到陣列的某個位置,然後利用陣列的“隨機訪問”特性進行快速查詢;通過雜湊函式對映值來存放資料的陣列就是雜湊表。

  • 為什麼要使用雜湊表

    在幾乎為O(1)的時間複雜度內快速查詢元素在陣列中的位置。與一般查詢不同,若直接在陣列內查詢某個元素,需要從頭遍歷一次陣列,其時間複雜度為O(n);若使用二分查詢,每次都需要將待查詢區間縮小一半,其時間複雜度為O(logn);

  • 雜湊表、陣列、連結串列的區別

    為了具有陣列“隨機訪問”的特性,在雜湊表中引入了雜湊函式,然而雜湊函式的引入會出現雜湊衝突,比如“如果兩個字串在雜湊表中對應的位置相同怎麼辦?”,而陣列的容量又是有限的,其中一種解決方案就是使用連結串列結構,在雜湊表的每個入口掛一個連結串列,儲存所有對應的字串就OK了,此時的雜湊表就是一種陣列+連結串列組合的資料結構。

3.2 雜湊(雜湊)函式

把任意長度的輸入(又叫做預對映, pre-image),通過雜湊演算法,變換成固定長度的輸出,該輸出就是雜湊值。這種轉換是一種壓縮對映,也就是,雜湊值的空間通常遠小於輸入的空間,不同的輸入可能會雜湊成相同的輸出,而不可能從雜湊值來唯一的確定輸入值。簡單的說就是一種將任意長度的訊息壓縮到某一固定長度的訊息摘要的函式。

3.3 衝突處理方法

  • 開放定值
  • 再雜湊|雜湊
  • 拉鍊法
  • 建立公共溢位區

https://blog.csdn.net/weixin_42637495/article/details/103192268

https://blog.csdn.net/hqd_acm/article/details/5901955

3.4 程式碼演示

// 雜湊函式:BKDRHash, 將 字串型別 -> int
// 衝突處理:拉鍊法

typedef struct Node {
    char *str;
    struct Node *next;
} Node;

typedef struct HashTable {
    Node **data;
    int size;
} HashTable;

Node *init_Node(char *str, Node *head) {
    Node *p = (Node *)malloc(sizeof(Node));
    p->str = strdup(str); // 深拷貝
    p->next = head;       // 頭插法
    return p;
}

HashTable *init_hash(int n) {
    HashTable *h = (HashTable *)malloc(sizeof(HashTable));
    h->size = n << 1; // 雜湊表的空間利用率,一般為70%~90%,當前利用率為50%
    h->data = (Node **)calloc(h->size, sizeof(Node *));
    return h;
}

int BKDRHash(char *str) {
    int seed = 31, hash = 0;
    for (int i = 0; str[i]; i++) hash = hash * seed + str[i];
    return hash & 0x7fffffff;
}

int insert(HashTable *h, char *str) {
    int hash = BKDRHash(str);
    int ind = hash % h->size;
    h->data[ind] = init_Node(str, h->data[ind]);  // 拉鍊法
    return 1;
}

int search(HashTable *h, char *str) {
    int hash = BKDRHash(str);
    int ind = hash % h->size;
    Node *p = h->data[ind];
    while (p && strcmp(p->str, str)) p = p->next;
    return p != NULL;
}

void clear_node(Node *head) {
    if (head == NULL) return ;
    Node *p = head, *q;
    while (p != NULL) {
        q = p->next;
        free(p->str);
        free(p);
        p = q;
    }
    return ;
}

void clear(HashTable *h) {
    if (h == NULL) return;
    for (int i = 0; i < h->size; i++) {
        clear_node(h->data[i]);
    }
    free(h->data);
    free(h);
    return ;
}

相關文章