做資料庫開發的程式員,可能每天都會處理各種各樣的查詢sql,這個就是尋找(Search)。通過查詢記錄主鍵欄位(即主關鍵碼)或其它非唯一欄位(即次關鍵碼)找到所需要的記錄。
如果在尋找的過程中,不改變未經處理資料(的資料結構),則這種尋找稱為靜態尋找(Static Search);如果找不到,需要向資料庫裡插入記錄(或者找到了,需要從資料庫裡刪除),這種在尋找過程中需要動態調整未經處理資料(的資料結構),這種尋找稱為動態尋找(Dynamic Search).
被尋找的資料結構(比如資料庫中的某張表)稱為尋找表,用於靜態尋找的稱為靜態尋找表,反之則稱為動態尋找表。
一、靜態尋找
因為靜態尋找中不需要刪除或新增記錄,所以用順序表比較適合。
1.1 順序尋找(Sequnce Search)
因為尋找表為線性結構,所以也被稱為線性尋找(Linear Search),其思路很簡單:從順序表的一端向另一端逐個掃描,找到要的記錄就返回其位置,找不到則返回失敗資訊(通常為-1)。
/// <summary> /// 順序尋找 /// </summary> /// <param name="arr">要尋找的順序表(比如數組)</param> /// <param name="key">要尋找的值</param> /// <returns>找到返回元素的下標+1,否則返回-1</returns> static int SeqSearch(int[] arr, int key) { int i = -1; if (arr.Length <= 1) { return i; } arr[0] = key;//第一個元素約定用來存放要尋找的值,這個位置也稱為“監視哨”,當然這並不是必須的,只是為了遵守原書的約定而已(以下同) bool flag = false; for (i = 1; i < arr.Length; i++) { if (arr[i] == key) { flag = true; break; } } if (!flag) { i = -1; } return i; }
這種全表掃描的方法,雖然很容易理解,但是效率是很低的,特別是表中記錄數很多時。如果尋找表的記錄本身是有序的,則可以用下面的辦法改進效率
1.2 二分尋找(Binary Search)
思路:因為尋找表本身是有序的(比如從小到大排列),所以不必傻傻的遍曆每個元素,可以取中間的元素與要尋找的值比較,比如尋找值大於中間元素,則要尋找的元素肯定在後半段;反之如果尋找值小於中間元素,則要尋找的元素在前半段;然後繼續二分,如此反覆處理,直到找到要找的元素。
/// <summary> /// 二分尋找(適用於有序表) /// </summary> /// <param name="arr">要尋找的有序表</param> /// <param name="key">要尋找的值</param> /// <returns>找到則返回元素的下標+1,否則返回-1</returns> static int BinarySearch(int[] arr, int key) { arr[0] = key;//同樣約定第一個位置存放要尋找的元素值(僅僅只是約定而已) int mid = 0; int flag = -1; int low = 1; int high = arr.Length - 1; while (low<=high) { //取中點 mid = (low + high) / 2; //尋找成功,記錄位置存放到flag中 if (key == arr[mid]) { flag = mid; break; } else if (key < arr[mid]) //調整到左半區 { high = mid - 1; } //調整到右半區 else { low = mid + 1; } } if (flag > 0) { return flag;//找到了 } else { return -1;//沒找到 } }
二分尋找效能雖然提高了不少,但是它要求尋找表本身是有序的,這個條件太苛刻了,多數情況下不容易滿足,那麼如何將上面的二種方法結合在一起,又保證效率呢?
1.3 索引尋找(Index Search)
思路:可以在尋找表中選取一些關鍵記錄,建立一個小型的有序表(該表中的每個元素除了記錄自身值外,還記錄了對應主尋找表中的位置),即索引表。尋找時,先到索引表中通過索引記錄大致判斷要尋找的記錄在主表的哪個地區,然後定位到主表的相應地區中,僅搜尋這一個區塊即可。
因為索引表本身是有序的,所以尋找索引表時,可先用前面提到的二分尋找判斷一般位置,然後定位到主表中,用順序尋找。
比如:要尋找值為78的記錄,先到索引表中二分尋找,能知道該記錄,應該在主表索引13至18 之間(即第4段),然後定位到主表中的第4段順序尋找,如果找不到,則返回-1,反之則返回下標。
所以該方法的關鍵在於索引的建立!以為例,在主表中挑選關索引值建立索引時,要求該關索引值以前的記錄都比它小,這樣建立的索引表才有意義。
其實該思路在很多產品中都有應用,比如資料庫的索引以及Lucene.Net都可以看作索引尋找的實際應用。
順便提一下:如果尋找主表記錄超級多,達到海量的層級,最終建立的索引表記錄仍然很多,這樣二分法尋找還是比較慢,這時可以在索引表的基礎上再建立一個索引的索引,稱之為二級索引,如果二級索引仍然記錄太多,可以再建立三級索引。
二、動態尋找
動態尋找中因為會經常要插入或刪除元素,如果用數組來順序儲存,會導致大量的元素頻繁移動,所以出於效能考慮,這次我們採用鏈式儲存,並介紹一種新的樹:二叉排序樹(Binary Sort Tree)
就是一顆“二叉排序樹 ”,其基本特徵是:
1、不管是哪個節點,要麼沒有分支(即無子樹)
2、如果有左分支,則左子樹中的所有節點,其值都比它自身的值小
3、如果有右分支,則右子樹中的所有節點,其值都比它自身的值大
2.1、二叉排序樹的尋找
思路:從根節點開始遍曆,如果正好該根節點就是要找的值,則返回true,如果要尋找的值比根節點大,則調整到右子樹尋找,反之調整到左子樹。
/// <summary> /// 二叉排序樹尋找 /// </summary> /// <param name="bTree"></param> /// <param name="key"></param> /// <returns></returns> static bool BiSortTreeSearch(BiTree<int> bTree, int key) { Node<int> p; //如果樹為空白,則直接返回-1 if (bTree.IsEmpty()) { return false; } p = bTree.Root; while (p != null) { //如果根節點就是要找的 if (p.Data == key) { return true; } else if (key > p.Data) { //調整到右子樹 p = p.RChild; } else { //調整到左子樹 p = p.LChild; } } return false; }
註:上面的代碼中,用到了BiTree<T>這個類,在資料結構C#版筆記--樹與二叉樹 中可找到,為了驗證該代碼是否有效,可用下列代碼測試一下:
//先建立樹 BiTree<int> tree = new BiTree<int>(100); Node<int> root = tree.Root; Node<int> p70 = new Node<int>(70); Node<int> p150 = new Node<int>(150); root.LChild = p70; root.RChild = p150; Node<int> p40 = new Node<int>(40); Node<int> p80 = new Node<int>(80); p70.LChild = p40; p70.RChild = p80; Node<int> p20 = new Node<int>(20); Node<int> p45 = new Node<int>(45); p40.LChild = p20; p40.RChild = p45; Node<int> p75 = new Node<int>(75); Node<int> p90 = new Node<int>(90); p80.LChild = p75; p80.RChild = p90; Node<int> p112 = new Node<int>(112); Node<int> p180 = new Node<int>(180); p150.LChild = p112; p150.RChild = p180; Node<int> p120 = new Node<int>(120); p112.RChild = p120; Node<int> p170 = new Node<int>(170); Node<int> p200 = new Node<int>(200); p180.LChild = p170; p180.RChild = p200; //測試尋找 Console.WriteLine(BiSortTreeSearch(tree, 170));
2.2、二叉排序樹的插入
邏輯:先在樹中尋找指定的值,如果找到,則不插入,如果找不到,則把要尋找的值插入到最後一個節點下做為子節點(即:先尋找,再插入)
/// <summary> /// 二插排序樹的插入(即:先尋找,如果找不到,則插入要尋找的值) /// </summary> /// <param name="bTree"></param> /// <param name="key"></param> /// <returns></returns> static bool BiSortTreeInsert(BiTree<int> bTree, int key) { Node<int> p = bTree.Root; Node<int> last = null;//用來儲存尋找過程中的最後一個節點 while (p != null) { if (p.Data == key) { return true; } last = p; if (key > p.Data) { p = p.RChild; } else { p = p.LChild; } } //如果找了一圈,都找不到要找的節點,則將目標節點插入到最後一個節點下面 p = new Node<int>(key); if (last == null) { bTree.Root = p; } else if (p.Data < last.Data) { last.LChild = p; } else { last.RChild = p; } return false; }
2.3 二叉排序樹的建立
從剛才插入的過程來看,每個要尋找的值,動態尋找一次以後,就會被附加到樹的最後,所以:"給定一串數字,將它們建立一棵二叉排序樹"的思路就有了,依次把這些數字動態尋找一遍即可。
/// <summary> /// 建立一顆二插排序樹 /// </summary> /// <param name="tree"></param> /// <param name="arr"></param> /// <param name="index"></param> static void CreateBiSortTree(BiTree<int> tree, int[] arr) { for (int i = 0; i < arr.Length; i++) { BiSortTreeInsert(tree, arr[i]); } }
2.4 二叉排序樹的節點刪除
這也是動態查詢的一種情況,找到需要的節點後,如果存在,則刪除該節點。可以分為幾下四種情況:
a.待刪除的節點,本身就是分葉節點
這種情況下最簡單,只要把這個節點刪除掉,然後父節點的LChild或RChild設定為null即可
b.待刪除的節點,只有左子樹
思路:將本節點的左子樹上移,掛到父節點下的LChild,然後刪除自身即可
c.待刪除的節點,只有右子樹
思路:將自身節點的右子樹掛到父節點的左子樹,然後刪除自身即可
d.待刪除的節點,左、右子樹都有
思路:這個要複雜一些,先找出自身節點右子樹中的左分支的最後一個節點(最小左節點),然後將它跟自身對調,同時將“最小左節點”下的分支上移。
以上邏輯綜合起來,就得到了下面的方法:
/// <summary> /// 刪除二叉排序樹的節點 /// </summary> /// <param name="tree"></param> /// <param name="key"></param> /// <returns></returns> static bool DeleteBiSort(BiTree<int> tree, int key) { //二叉排序樹為空白 if (tree.IsEmpty()) { return false; } Node<int> p=tree.Root; Node<int> parent = p; while (p!=null) { if (p.Data == key) { if (tree.IsLeaf(p))//如果待刪除的節點為分葉節點 { #region if (p == tree.Root) { tree.Root = null; } else if (p == parent.LChild) { parent.LChild = null; } else { parent.RChild = null; } #endregion } else if ((p.RChild == null) && (p.LChild != null)) //僅有左分支 { #region if (p == parent.LChild) { parent.LChild = p.LChild; } else { parent.RChild = p.LChild; } #endregion } else if ((p.LChild == null) && (p.RChild != null)) //僅有右分支 { #region if (p == parent.LChild) { parent.LChild = p.RChild; } else { parent.RChild = p.RChild; } #endregion } else //左,右分支都有 { //原理:先找到本節點右子樹中的最小節點(即右子樹的最後一個左子節點) #region Node<int> q = p; Node<int> s = p.RChild; while (s.LChild != null) { q = s; s = s.LChild; } Console.WriteLine("s.Data=" + s.Data + ",p.Data=" + p.Data + ",q.Data=" + q.Data); //然後將找到的最小節點與自己對調(因為最小節點是從右子樹中找到的,所以其值肯定比本身要大) p.Data = s.Data; if (q != p) { //將q節點原來的右子樹掛左邊(這樣最後一個節點的子樹就調整到位了) q.LChild = s.RChild; } else //s節點的父節點就是p時,將s節點原來的右樹向上提(因為s已經換成p點的位置了,所以這個位置就不需要了,直接把它的右樹向上提升即可) { q.RChild = s.RChild; } #endregion } return true; } else if (key>p.Data) { parent = p; p = p.RChild; } else { parent = p; p = p.LChild; } } return false; }