1. 程式人生 > >第7章 查詢(散列表)

第7章 查詢(散列表)

  在前6章中學習幾種基本型別的資料結構,其中也有一些查詢的操作,第7章就是專門講比較具體的查詢演算法,還有各種優化。

  首先,順序查詢和折半查詢是比較熟悉的,平常用處挺大的,不過兩種查詢方法都有很明顯的劣勢,而折半查詢時間複雜度O(log2N),相對來說查詢效率比較高,但它只限於有序表。為了解決查詢演算法的侷限性:1.只能有序儲存;2.動態插入問題;3.資料量太大等等;有了後面的二叉排序樹、平衡二叉樹、B-樹、B+樹和散列表(雜湊查詢法),逐步深入,演算法難度也逐漸增加。

  二叉排序樹就實現了動態插入的功能,根據它的性質,我們在資料插入到二叉樹中的時候,已經進行了相當於一次有序遍歷,插入的過程基本是查詢,時間複雜度為O(log2

N)。之前的一道實踐題,就運用了二叉排序樹的性質(一串相同資料,輸入順序不同,建立的樹結構可能不同),判斷建立的樹是否為同一棵樹。

是否同一棵二叉搜尋樹 這道題主要是在插入的演算法,也是查詢時候的關鍵,同時也可以看出二叉搜尋樹也有不足之處,當有很大資料量的時候,n很大,查詢時間也增加。所以,另外一種查詢演算法,散列表查詢法(Hash Search)理論說是可以實現O(1)的時間複雜度,還是比較有爭議的。問題出現在不同元素值有著相同的雜湊值,然後需要處理這些碰撞,把它們分別放到不同的地址,所以在查詢的時候也需要通過一定時間來遍歷,找到需要的資訊的地址。

  而且有一種最差的情況就是,我們採用鏈地址法處理衝突,比如向其中連續插入n個元素。這n個元素的值均不相同,但是全部有相同的雜湊值,也就是說會被插入到同一個位置上去。(如:2、4、6、8、10.....) 每一次插入時,首先都找到相同的地址,然後判斷現在這個值確實不在表中,就將新的元素加到這個雜湊地址後面的單鏈表,這樣一來,插入第1個元素需要1次比較操作;插入第2個元素需要1+1次比較操作;……;插入第n個元素時需要n次比較操作。最後總共需要1+2+…+n=0.5n(n+1)=O(n2

)個常數時間操作,才能把n個元素插入到雜湊表中。所以這樣插入的時間複雜度要O(n2),查詢時候的遍歷操作O(1)覺得不太可能。

  不過在建立雜湊表的時候避免大量碰撞還是可以實現的。處理衝突的方法有開放地址法和鏈地址法,開放地址法中有線性探測法和二次探測法,它們幾個優劣之分,具體應用要具體選擇合適的方法。在這一章練習題,是採用了二次探測法(增量為正)。

 Hashing

The task of this problem is simple: insert a sequence of distinct positive integers into a hash table, and output the positions of the input numbers. The hash function is defined to be H(key) = key % TSizeH(key)=key%TSize where TSizeTSizeis the maximum size of the hash table. Quadratic probing (with positive increments only) is used to solve the collisions.

Note that the table size is better to be prime. If the maximum size given by the user is not prime, you must re-define the table size to be the smallest prime number which is larger than the size given by the user.

Input Specification:

Each input file contains one test case. For each case, the first line contains two positive numbers: MSizeMSize (\le 10^4≤10​4​​) and NN (\le MSize≤MSize) which are the user-defined table size and the number of input numbers, respectively. Then NN distinct positive integers are given in the next line. All the numbers in a line are separated by a space.

Output Specification:

For each test case, print the corresponding positions (index starts from 0) of the input numbers in one line. All the numbers in a line are separated by a space, and there must be no extra space at the end of the line. In case it is impossible to insert the number, print "-" instead.

Sample Input:

4 4

10 6 4 15

Sample Output:

0 1 4 -

 剛剛開始做題目沒看清楚,以為如果有衝突就將後面的數記錄為無法插入,後來才發現要使用二次探測法處理衝突,解題過程思路重新整理了一次,我的思路比較直接,想把資料對應的雜湊地址用一個數組記錄起來,遍歷這個陣列就可以知道相應資料的地址資訊或者無法插入。

在輸入資料同時並將結果順便算出的版本:

 

#include<iostream>
using namespace std;
int visit[10000]={0};//訪問標誌陣列

int prime(int a)//判斷是否為素數
{
    int i;
    if(a<=1) return 0; //這一步判斷很重要,題目有一個測試點就是檢測最小值
    for(i=2;i<a;i++)
    {    
        if(a%i==0)
        return 0;    //不是素數返回0
    }
    if(a>=i)
        return a;//是素數
}

void hashlocate(int m,int n,int c)
{    
    int i,h,d,data;
    
    for(i=0;i<n;i++)
    {
        cin>>data;
        d=0;//每次輸入資料,d為增量,從0增加
        h=data%c; d++;//先計算第一個值對應的散列表地址(下標)
        if(i) cout<<" ";//第一個輸出前不帶空格
        
      while(d<m&&visit[h])//迴圈是對已經訪問過的地址進行操作,直到找到可放入的位置
        {
            h=(data+d*d)%c;d++;
        }
        
        if(!visit[h]) //對未訪問過地址進行輸出
        {
            cout<<h;
            visit[h]=1;//輸出訪問值之後對應標誌陣列位置記為1
        }
        else 
            cout<<'-'; //在while之後仍沒有位置的資料,表示無法插入
    }
}

int main()
{
    int m,n,c;
    cin>>m>>n;
    c=m;
    while(prime(c)==0)//如果輸入的數不是素數,找到大於此數的最小素數
    {
        c++;    
    }

    hashlocate(m,n,c);
    return 0;
} 
View Code

 

 

 

題目有一步是找一個合數的下一個緊接著的素數,開始覺得挺麻煩的,後來發現將判斷一個數是否為素數的程式碼改一下就好,所以我的方法是用一個while進行計算。

用輔助陣列儲存地址後在輸出的版本:

#include<iostream>
using namespace std;
int visit[10000]={0};

void Hashlocate(int m,int n)
{
    int i,flag=0,h,d;
    int *H1,*H;
    H1=new int[n];
    H=new int[n];
    for(i=0;i<n;i++)
    {    
        cin>>H1[i];
        h=H1[i]%m;//先計算第一個值對應的散列表地址(下標)
        d=0;d++;//每次輸入資料,d為增量,從0增加
        
        while(d<m&&visit[h])//迴圈是對已經訪問過的地址進行操作,直到找到可放入的位置
        {
            h=(H1[i]+d*d)%m;d++;
        }
        
        if(!visit[h])//對未訪問過地址進行輸出
        {
            H[i]=h;visit[h]=1;//將地址儲存到輔助陣列,輸出訪問值之後對應標誌陣列位置記為1
        }
        else H[i]=-1;//經過二次探測法,仍然無位置的,標記一下無法插入的資料 
    }
    
    for(i=0;i<n;i++)
    {
        if(H[i]!=-1)
        {
            if(flag==0)//處理空格輸出 
            {
                cout<<H[i];flag=1;
            }
            else cout<<" "<<H[i];
        }
        else
        cout<<" "<<'-';//沒有位置可放的資料,表示無法插入
    }
    
}

int prime(int a)//判斷是否為素數
{
    int i;
    if(a<=1) return 0;//這一步判斷很重要,題目有一個測試點就是檢測最小值
    for(i=2;i<a;i++)
    {    
        if(a%i==0)
        return 0;    
    }
    if(a>=i)
        return a;
}


int main()
{
    int m,n;
    cin>>m>>n;
    while(prime(m)==0)//如果輸入的數不是素數,找到大於此數的最小素數
    {
        m++;    
    }

    Hashlocate(m,n);
    return 0;
} 
View Code

 

 

遇到的困難是在增量那裡的計算,對於每個輸入資料,要求出它的雜湊地址,如果衝突,要用二次探測法重新尋找位置,這一點思考許久,最後又是用了while輔助計算,結合visit陣列和表長m來判斷,debug幾次後,測試幾組資料才做好這一步。還有一個細節問題,卡了我很長時間一直沒注意到,就是測試點2 最小值,因為在判斷素數的函式中忽略了最小值1,沒有處理,一直檢查其他部分是否出錯,等到最後才知道是細節問題,自己對問題還是沒有考慮周全,以後需要注意這一點。

 

&n