基於GeoHash算法的附近點搜索實現(一)


1. 引入

最近在參加學校的計算機仿真大賽,時間好像有點不夠,所以只完成了前面的一部分最基礎的功能,中途還是選擇了放棄。但是之前的部分的確覺得完成得還不錯,在這里分享一下。題目是要完成一個宇宙飛船加油點的分配調度系統。完成的部分是給定坐標附近點的搜索。

2. 算法使用原因

我們要完成的一個是二維附近點搜索的算法。就是在給出若干個加油點的二維坐標,然后再給你一個當前坐標,你要搜索出距離當前坐標最近的一個加油站的坐標點。在考慮二維的附近點搜索時,最原始的方法肯定是將所有的加油點的坐標都加入到list中.然后遍歷所有的節點,判斷哪個節點的坐標距離自己最近。但是這樣操作的話,由於我們要進行多次的附近點搜索,這樣每次搜索的成本就會相當大,比如我們搜索 N次,一共有 M 個加油點,復雜度將達到 N*M ,降低搜索效率。
所以我們應該降低每次的搜索效率。然后想過要使用建立哈希表進行搜索,這樣成本基本花在了建表上,搜索所花費的時間就會少很多。但是按照自己的想法,去建一個哈希表的確比較困難,而且當地圖比較大時,空間復雜度會十分的高,因此這種方法還是需要改進。然后我查閱了一些資料,參閱到一些附近點搜索的經典算法,也就是將要介紹的GeoHash算法。能夠將坐標變成特定的編碼,然后進行對應哈希,還能夠根據編碼的前綴,來進行判斷兩點是否在附近。

3. 算法介紹

在一篇很經典的文章中有所介紹GeoHash下附鏈接:
http://blog.nosqlfan.com/html/1811.html
此博客中,簡要地說明GeoHash算法的思想:

  1. 將地圖四分,也就是分成左上、右上、左下、右下四個部分,然后對應的地圖塊的編碼后面追加"01","11","00","10"
    |01|11|
    |---|---|
    |00|10|
  2. 然后再將四分后的各個地圖塊,重復步驟1,不斷地進行四分,編碼也會兩位兩位地進行增加。直到地圖塊不能再進行四分。
  3. 得到對應的每個坐標的編碼。

這就是GeoHash最基礎的算法,能夠將地圖上的每個坐標點都進行編碼。然后根據這些編碼,很快就可以發現一個規律,因為是按着每個地圖4分的,所以這地圖四分前的編碼是相同的,即使4分之后,這4塊地圖還是有着相同的前綴,因此,我們可以根據編碼的最長相同前綴,去找出距離最近的加油站的坐標。
但是!這個算法還是有很大的缺點的:
由於GeoHash是將區域划分為一個個規則矩形,並對每個矩形進行編碼,會導致以下問題,比如紅色的點是我們的位置,綠色的兩個點分別是附近的兩加油點,但是在查詢的時候會發現距離較遠加油站的GeoHash編碼與我們一樣(因為在同一個GeoHash區域塊上),而較近加油站的GeoHash編碼與我們不一致。這個問題往往產生在邊界處。
錯誤情況
因此,我們需要對算法進行改進,解決的思路很簡單,我們查詢時,除了使用定位點的GeoHash編碼進行匹配外,還使用周圍8個區域的GeoHash編碼,這樣可以避免這個問題。

4. 需要解決的問題

  1. 坐標值轉化為GeoHash編碼值
  2. 根據當前區域的GeoHash,推算出周圍8個方位區域塊的的GeoHash值。
  3. 將這8個區域塊中所有加油點進行儲存,並且一一計算它們到當前坐標的距離,並且計算出最短距離的點。
  4. 考慮存儲結構,以及算法實現。

5. 算法實現

因為項目開發的時候要可視化,所以當時就選擇了使用C#。接下來的實現代碼,都是C#編寫的。

1. 坐標值轉化為GeoHash

首先我們要知道當前地圖大小為多少,取橫坐標、縱坐標的中值,然后區分出4個區域,然后按照坐標所落到的區域,將對應的兩位編號追加到地圖的編碼后,然后再將當前地圖橫坐標、縱坐標、都除以二,加上根據分塊后地圖改變的編號,以及坐標改變值,作為參數繼續遞歸。遞歸結束的條件為地圖的精度都已經減少為1的坐標的。
以下為實現代碼:

//xb,yb分別為地圖的橫坐標和總坐標大小
//a,b,分別為給定點的橫坐標、以及縱坐標
//code,為存儲編碼的字符串
public static void Encode(StringBuilder code, int xb, int yb, int a, int b)
{
    if (xb == 1 && yb == 1)
        return;
    if (a < xb / 2 && b < yb / 2)
    {
        code.Append("00");
        Encode(code, xb / 2, yb / 2, a, b);
    }
    else if (a < xb / 2 && b >= yb / 2)
    {
        code.Append("01");
        Encode(code, xb / 2, yb / 2, a, b - yb / 2);
    }
    else if (a >= xb / 2 && b < yb / 2)
    {
        code.Append("10");
        Encode(code, xb / 2, yb / 2, a - xb / 2, b);
    }
    else if (a >= xb / 2 && b >= yb / 2)
    {
        code.Append("11");
        Encode(code, xb / 2, yb / 2, a - xb / 2, b - yb / 2);
    }
    return;
}

2. 根據當前的編碼,計算其余8個方位的編碼

這個的實現其實也很簡單。首先,我們其實先只需要搜尋實現上下左右4個方位的編碼,那么根據一些小組合,剩下的8個方位,也同樣能夠計算得到了。
那么我們就要計算4個方位毗鄰的編碼。情況其實也很簡單。
有一些區域的上級區域,與自己並不相同,因此前綴也肯定不相同。所以我們判斷到上級區域不相同時,就要進行對上級區域的轉變。但是要對上級區域進行改變的時候,發現上級的上級區域也不同,那又要再深一層地去改變。這樣一直下去,就會同樣形成一個遞歸的過程。用語言很那解釋,但是大家把各個坐標的編碼表都計算出來並且顯示后,就很容易能夠找到規律。
下面貼上代碼供大家參考。

        public static void FindRight(StringBuilder code, int len)
        {
            if (len <= 0)
                return;
            if (code[len - 2] == '1')
            {
                FindRight(code, len - 2);
                code[len - 2] = '0';
            }
            else
            {
                code[len - 2] = '1';
            }
            return;
        }
 
        public static void FindLeft(StringBuilder code, int len)
        {
            if (len <= 0)
                return;
            if (code[len - 2] == '0')
            {
                FindLeft(code, len - 2);
                code[len - 2] = '1';
            }
            else
            {
                code[len - 2] = '0';
            }
            return;
        }
 
        public static void FindDown(StringBuilder code, int len)
        {
            if (len <= 0)
                return;
            if (code[len - 1] == '1')
            {
                FindDown(code, len - 2);
                code[len - 1] = '0';
            }
            else
            {
                code[len - 1] = '1';
            }
            return;
        }
 
 
        public static void Findup(StringBuilder code, int len)
        {
            if (len <= 0)
                return;
            if (code[len - 1] == '0')
            {
                Findup(code, len - 2);
                code[len - 1] = '1';
            }
            else
            {
                code[len - 1] = '0';
            }
            return; 
        }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM