次優查找樹的原理是什麼?

嚴蔚敏的書上講到了次優查找樹,但我不明白是基於什麼原理構造的次優查找樹?我也不懂次優查找樹是如何做到提高查找效率的?求大神講解


花了一晚上時間搞清楚了,代碼也寫出來了。先佔個坑,明早來答

===========我是分割線==============================

讓我這個數學渣用通俗的語言來解釋一下,希望你能看懂。

1、次優查找樹是折半查找的一種一般形式,其理論基礎是「被查找的各元素是不等概的」,而折半查找就是等概的,我們在使用中默認了這一性質。

比如,對於有序數組

int a = {1,2,3,4,5};

用折半查找時,應該現比較最中間的3,如果如果待查整數等於3,查找結束。如果小於3,就繼續在左邊的部分數組裡查找;反之,在右邊的數組裡查找。

問題在於,我們為什麼不從4開始找呢?為什麼不從1開始呢?

因為在等概率的情況下,這樣能讓整體的平均搜索的長度(也就是次數)最小,實際也是二分查找樹的深度最小。因為相同結點數的二叉樹,越是豐滿的二叉樹高度越小。也就是說,每個節點的左右子樹的高度差最小,二叉樹的高度就越小,查找越底層的元素所需要的路徑長度就越短,比較次數也越少(相同結點數完全二叉樹的深度小於等於其他形態的二叉樹的深度)。

我的ubuntu不好畫圖,給你個鏈接看看,你自己也可以畫一下圖。具有12個關鍵字的有序表,折半查找的平均查找長度()_牛客網。你把每個元素查找成功時的路徑長度加起來看看,是不是完全二叉樹最小?

2、現在我告訴你,每個元素被查找的概率是不一定相同的。剛才的辦法還是最佳的嗎?

比如按照剛才的查找樹,元素5是最後一個,按照折半查找的話,每次查找都要花費3次比較才能找到,然而元素5被查找的概率是0.8,也就是說查了10000次,可能8000次都是它,那麼這8000次查找就用了 times = 8000 * 3 = 24000次查找。但是如果把5放到查找樹的根節點位置,那麼是不是只需要8000次比較就行了?

所以,對於每個元素被查找的概率不同的情況下,折半查找不是最佳方法!

3、如果僅僅考慮查找成功的情況,構造一顆靜態 最優查找樹 的性能是最好的。

用數學公式來表示就是:使得PH = sum_{i=1}^{n}{omega_{i}h_{i}} 的PH值最小的樹為該數組的靜態最優查找樹。其中i為節點標號,omega _{i}為節點i的帶權路徑長度,omega _{i} = c _{i} * p _{i},它等於結點i的查找路徑長度c,乘以該結點被查找的概率p;h表示節點i在搜索樹中的高度。

通俗點來說,就是權值越大的結點,越放到靠近根結點的位置!這個很好理解,這種結點要麼查找概率大,要麼需要的比較次數(折半查找中的次數)多,要麼以上兩者都佔了,當然應該越早查越好啊,對不對?

然並卵,這種樹的構造時間複雜度為Theta (n^{3}),等你構造出來,天早就黑了好么……

關於為什麼它的時間複雜度這麼高,請參考http://www.cs.rutgers.edu/~kaplan/503/handouts/optimalbst.html或者Dynamic Programming

4、那麼腫么辦呢?我們來構造 次優查找樹。

書上首先就告訴你了,這種樹的查找效率很少低於最優查找樹的3%。

核心:選出一個結點,使得它左右兩側的子數組的權值累加和之差的絕對值最小。把這個結點當做根節點,遞歸地用剛才的左右字數組構造它的左右子樹。

數學表達式:Delta P_{i} = |sum_{j=i+1}^{h}{omega _{j}} - sum_{j=l}^{i-1}{omega _{j}}|

之前的折半查找樹是為了讓左右子樹的高度差盡量小,現在你就把高度的概念替換為權值之和來理解,就好了。為什麼要讓左右子樹的權值累加和之差最小?為了使樹的深度最小。

說了這麼多,下面來上代碼。請從主函數開始看,應該算是簡單易懂

bitree.h 這裡是樹的二叉鏈表和各種遍歷列印的定義。

#include &
#include &
#include &
#include &
#include &
using namespace std;

typedef struct TNode
{
int data;
struct TNode* lchild;
struct TNode* rchild;
}BiTree, *pBiTree;

void creat_tree(pBiTree rt)
{
char ch;
ch=getchar();
if("#"==ch)
{
rt=NULL;
}
else
{
rt=(pBiTree)malloc(sizeof(BiTree));
rt-&>data=ch;
creat_tree(rt-&>lchild); //構造左子樹
creat_tree(rt-&>rchild); //構造右子樹
}
}

void PreOrderPrint(pBiTree rt)
{
cout &<&< "PreOrderPrint: "; if(!rt) return; stack& s;
s.push(rt);
while(!s.empty())
{
pBiTree cur = s.top();
cout &<&< (char)(cur-&>data);
s.pop();
if(cur-&>rchild)
s.push(cur-&>rchild);
if(cur-&>lchild)
s.push(cur-&>lchild);
}
cout &<&< "@" &<&< endl; } void InOrderPrint(pBiTree rt) { cout &<&< "InOrderPrint: "; if(!rt) return; stack& s;
pBiTree cur = rt;

while(!s.empty() || cur != NULL)
{
while(cur)
{
s.push(cur);
cur = cur-&>lchild;
}
if(!s.empty())
{
cur = s.top();
cout &<&< (char)(cur-&>data);
s.pop();
cur = cur-&>rchild;
}
}
cout &<&< "@" &<&< endl; } bool visit(pBiTree node) { if(node) { cout &<&< char(node-&>data);
return 1;
}
else
return 0;
}

void LevelOrderPrint(pBiTree rt)
{
cout &<&< "LevelOrderPrint: "; if(!rt) { cout &<&< "@" &<&< endl; return; } queue& q;
q.push(rt);
pBiTree cur = NULL;

while(!q.empty())
{
cur = q.front();
q.pop();
if(visit(cur))
{
if(cur-&>lchild)
q.push(cur-&>lchild);
if(cur-&>rchild)
q.push(cur-&>rchild);
}
}
cout &<&< "@" &<&< endl; }

second_optimal.cpp 這裡是創建次優二叉樹的實現

//second optimal tree
//suanfa93.cpp
#include &
#include &
#include &
#include "bitree.h"
using namespace std;

void AssignVal(int* val, int low, int high, int factor)
{
if(!val || low &< 0 || low &> high)
return;
if(low == high)
{
val[low] = factor;
return;
}
int mid = (low + high) / 2;
val[mid] = factor;
AssignVal(val, low, mid-1, factor+1);
AssignVal(val, mid+1, high, factor+1);
}

int* SearchLength(int len)
{
if(len &<= 0) return NULL; int* sl = (int*)malloc(sizeof(int) * len); if(!sl) exit(0); AssignVal(sl, 0, len-1, 1); return sl; } float* SumWeight(int* nodes, float* prob, int size) { float* sw = (float*)malloc(sizeof(float) * size); if(!sw) exit(0); float before = 0.0; for(int i = 0; i &< size; i++) { sw[i] = nodes[i] * prob[i] + before; before = sw[i]; } return sw; } void SecondOptimal(pBiTree rt, int* nodes, float* sw, int low, int high) { if(!nodes || !sw || low &< 0 || low &> high)
return;
int i = low;
float min = fabs(sw[high] - sw[low]);
float dw = sw[high];
for(int j = low + 1; j &<= high; ++j) { float tmp = fabs(dw - sw[j] - sw[j-1]); if(tmp &< min) { i = j; min = tmp; } } rt = (pBiTree)malloc(sizeof(BiTree)); if(!rt) exit(0); rt-&>data = nodes[i];
if(i == low)
rt-&>lchild = NULL;
else
SecondOptimal(rt-&>lchild, nodes, sw, low, i-1);
if(i == high)
rt-&>rchild = NULL;
else
SecondOptimal(rt-&>rchild, nodes, sw, i+1, high);
}

int main(int argc, char const *argv[])
{
//1、已知條件,待查找的有序數組和數組中各元素被查找的概率(不等概率)
const int size = 5;
int nodes[size] = {"1","2","3","4","5"};
float probability[size] = {0.2,0.3,0.2,0.1,0.2};

//2、根據數組,求出每個元素查找成功時的平均查找長度(次數),存儲在schlen數組中。
int* schlen = SearchLength(size);

//3、求出每個元素的權值sw,由待查數組nodes和schlen中下標相對的元素相乘得到。
float* sw = SumWeight(schlen, probability, size);

//4、然後根據書中的演算法,遞歸的構造次優查找樹
pBiTree root;
SecondOptimal(root, nodes, sw, 0, size - 1);

//5、用前序、中序、層序遍歷把次優查找樹列印出來看看
PreOrderPrint(root);
InOrderPrint(root);
LevelOrderPrint(root);
return 0;
}

可以證明,當元素等概時,次優查找樹退化為一般的折半查找,構造次優查找樹的時間複雜度為Theta (n*logn),比起最優查找樹,它的時間複雜度是可接收的。


首先理清幾個概念

(二叉)查找樹:對有序數據進行查找,任何一種基於比較的查找策略都可以用一棵二叉樹表示。每次查找過程就是從這棵二叉樹的根節點出發,每次根據比較結果決定是走向左子樹還是右子樹還是停下來(因為已經找到了)。

查找樹的效率:如果你又已知了每個元素可能被查找的概率,你就可以對一棵查找樹計算它的期望比較次數。

最優查找樹:在已知概率的情況下,期望比較次數最小的樹。可以通過動態規劃求出。

次優查找樹:我不太喜歡這個譯名。一般說「次優」意思是「第二優」,但是這裡的「次優」的意思是「有時候就是最優,有時候是第二優,有時候我也不知道,總之,其實我什麼都不知道」。

雖然什麼都不知道,但是目標還是清楚的,就是希望在已知概率的情況下盡量減少構造出來的二叉查找樹的期望比較次數。然後有時候動態規劃太慢了,你就直接上貪心求一個還湊合的解就好了。


由於有序表各元素被訪問的概率不同,從而二分查找的效率不一定是較高的。因為平均查找長度ASL=∑Pi*Ci,我們根據ASL做查找演算法的性能分析。


終於在知乎看到關於次優原理的討論了,根據中國的易學等理論,構造出:次最大化原則---就是每一個利益對象,首先把自身的利益按照程度,分成等級,然後,以次於最大化的那個選項為第一考慮(第二最優,說起來拗口)。並且在協作系統內部達成共識,最小投入,集體收益達到最大,多贏模式的實現。誰都不追求自身最大的的那個利益極端值,最後大家都收穫了非常滿意的的收益,共同對社會的風險值總和最後:不敢獲最大,故能成最大。有些合乎老子思想的意味。


推薦閱讀:

RSA的公鑰和私鑰到底哪個才是用來加密和哪個用來解密?
如何循序漸進的學習數據挖掘?
ACM書籍推薦?
求10的一億次方對較小整數p取余的餘數?
oj上演算法題思路正確,程序也跑的起來,但是為了ac搞幾個小時,這樣有意義嗎?

TAG:程序員 | 演算法 | 編程 | C編程語言 | 數據結構 |