算法學習(一)查找算法

查找

1、什麼是查找?

在這裏插入圖片描述
一個比較有用的信息就是:查找算法沒有穩定性問題、查找算法的設計需要結合特定的數據結構以及一些操作來考慮。

2、查找算法如何分類

查找算法按照查找表是否有序的情況,可以分爲:

  • 無序查找:被查找數列有序無序均可;
  • 有序查找:被查找數列必須爲有序數列。

按照對查找表施加的動作,可以分爲:

  • 靜態查找:只對查找表進行查找操作;
  • 動態查找:對查找表進行查找操作,還可以對查找表數據進行修改。

3、如何評估查找算法的效率

衡量查找算法的效率,依據是平均查找長度(Average Search Length,ASL),指查找過程中關鍵字的平均比較的次數。使用公式表示:

在這裏插入圖片描述

4、具體算法類型及其實現

4.1 順序查找(線性查找)

顧名思義,就是按照查找表,從左到右或者從右到左順序查找。
java代碼

public static int sequenceSearch(int a[], int value, int n) {
		for(int i=1;i<n;i++) {
			if(a[i]==value) {
				return i;
			}
		}
		return -1;
	}

複雜度分析:時間複雜度O(n)
ASL = 1/n(1+2+3+…+n) = (n+1)/2

4.2 二分查找(折半查找)

二分查找也叫折半查找,就是通過將key(要查找的值)已經排好序的查找表中的中間值比較,根據結果來判斷:

  • 等於該中間值,則查找到,返回位置mid
  • 大於該中間值,則在左半部分繼續類似的查找
  • 小於該中間值,則在右半部分繼續類似的查找

java代碼

/* 迭代版*/
public static int binarySearch(int []a,int value,int n){
	int low=0;
	int high=n-1;
	int mid=0;
	while(low<high){
		mid = low+(high-low)*1/2;//每次都是比較中間的值
		if(a[mid] == value) {//等於
				return mid;
			}
			if(a[mid] > value) {//大於,則在左半部分查找
				high = mid - 1;//只改變high
			}
			else {//反之在右半部分查找
				low = mid + 1;//只改變low
			}
		}
	return -1; //查找失敗,return low,low表示此時如果要執行插入,插入的位置
	
}

/*遞歸版*/
public int binarySearch(int[] a, int target, int low, int high) {
	if (low > high) { return low; //查找失敗,low表示此時如果要執行插入,插入的位置
	int mid = low + (high - low) / 2;//中間值
	if (a[mid] > target) {//大於,查找左半部分
		return binarySearch(a,target,low,mid-1);
	} else if (a[mid] < target) {//小於,查找右半部分
		return binarySearch(a,target,mid+1,high);
	} else {
		return mid;//查找成功
	}
}

複雜度分析:時間複雜度爲O(log2n),對比順序查找,對於一個有1024個元素的數組,在最壞的情況下,二分查找法只需要比較log2n + 1= 11次,而在最壞的情況下線性查找要比較1023次。

注意事項

  • 查找表是有序的!對於靜態查找,該算法的效率不錯。但是如果是動態查找,因爲查找過程需要進行插入或者刪除操作,這就要求每次查找前需要數據重新排序,從而導致效率降低!
  • 使用high+low時要注意,如果是int類型,要注意是否溢出,即超過Integer.MAX_VALUE

4.3 插值查找(優化二分查找)

比較簡單的理解,就是我們不再使用二分比例(1/2),而是根據要查找的數據其大小在整個數列的大概比例從而判斷其左半部分或者右半部分的範圍。如1、2、3、4......10——a0到a9,現在要查找的數是8,low=0,high=9;(8-1)/(10-1)=7/9,則mid = low+(high-low)*7/9=0+7=7,所以我們應該在[7,9]或者[0,6]的範圍找,而不是[0,4],[5,9]

//二分查找
mid = low+(high-low)*1/2;//每次都是比較中間的值,比例是1/2

//插值查找
mid = low + (high-low) * (value-a[low]) / (a[high]-a[low]);//根據數據,確定比例
public static int insertionSearch(int a[], int value, int n) {
		int low = 0;
		int high = n-1;
		int mid = 0;
		while (low <= high) {
			mid = low + (high-low) * (value-a[low]) / (a[high]-a[low]);
			if(a[mid] == value) {
				return mid;
			}
			if(a[mid] > value) {
				high = mid - 1;
			}
			else {
				low = mid + 1;
			}
		}
		return -1;
	}

複雜度分析:查找成功或者失敗的時間複雜度均爲O(log2(log2n))。
注意事項

  • 查找表依舊需要有序
  • 查找的效率很依靠數據分佈的均勻性。關鍵字分佈又比較均勻的查找表來說,插值查找算法的平均性能比折半查找要好的多。反之,數組中如果分佈非常不均勻,那麼插值查找未必是很合適的選擇。

4.4 Fibonacci查找(斐波那契數列)

1、Fibonacci數列是什麼?
斐波那契數列,又稱黃金分割數列,指的是這樣一個數列:1、1、2、3、5、8、13、21、····,在數學上,斐波那契被遞歸方法如下定義:F(1)=1,F(2)=1,F(n)=f(n-1)+F(n-2) (n>=2)。該數列越往後相鄰的兩個數的比值越趨向於黃金比例值(0.618)。
2、Fibonacci查找
斐波那契查找也叫做黃金分割法查找,它也是根據折半查找算法來進行修改和改進的。因爲二分查找或者插值查找都要用到除法,而Fibonacci查找只需要用到加法減法。
3、算法是如何實現查找的?

  • 建立斐波那契數列F(n),如 1 1 2 3 5 8 13…
  • 根據查找表(data數組)的長度,找到略大於data.length的F(n),如data的長度爲10,則找到F(7)=13;
  • 建立長度爲F(n)的臨時查找表temp[F(n)],將data的值一一複製到temp對應位置,temp中剩下的元素用data中最後的元素填充。如temp[13],前10個數對應data裏邊的數,temp[10]、temp[11],temp[12]都賦值爲temp[9]
  • 利用查找規則,進行查找。

查找規則:
F[n]個元素分割爲前半部分F[n-1]個元素,後半部分F[n-2]個元素,找出要查找的元素在那一部分並遞歸,直到找到。
圖解:
在這裏插入圖片描述

java代碼

  /**
     * 創建斐波那契數列
     * @return 斐波那契數列
     */
    private static int[] fibonacci(){
        int n = 30;//默認創建100項
        int []f = new int[n];
        f[0]=1;
        f[1]=1;
        for(int i=2;i<n;i++){
            f[i]=f[i-1]+f[i-2];
        }
        return f;
    }

    private static int fibonacciSearch(int []data,int value) {
        int []f = fibonacci();//獲取斐波那契數列
        int low =0,mid=0;
        int len = data.length;
        int high = len-1;

        //1、尋找f(n)略大於數組長度
        int k=0;
        while(len>f[k]){
            k++;
        }
        //2、創建臨時數組
        int []temp = new int[f[k]];
        for(int i=0;i<f[k];i++){
            if(i<len)
                temp[i]=data[i];
            else temp[i]=data[len-1];
        }
      /*  for(int i:temp){
            System.out.print(i+" ");
        }
        System.out.println();*/

        //3、開始查找
        while(low<=high){
            mid = low+f[k-1]-1;
            if(temp[mid]<value){//右半部分
                low = mid+1;
                k = k-2;//f(n)=f(n-1)+f(n-2);
            }else if(temp[mid]>value){//左半部分
                high = mid-1;
                k = k-1;
            }else{
                if(mid<=high)//查找到了
                    return mid;
                else
                    return high;//但是位於temp填充的部分,等於high
            }

        }
        return -1;//查找失敗。
    }

在這裏插入圖片描述

複雜度分析
同樣,斐波那契查找的時間複雜度還是O(log2n),但是與折半查找相比,斐波那契查找的優點是它只涉及加法和減法運算,而不用除法,而除法比加減法要佔用更多的時間,因此,斐波那契查找的運行時間理論上比折半查找小,但是還是得視具體情況而定。

4.5 樹表查找(樹結構)

4.5.1 二叉樹查找

步驟
1、構建二叉查找樹
將需要查找的數據建立二叉樹,小於雙親結點的爲左子樹,大於等於雙親結點的爲右子樹。
2、根據要查找的樹與結點值比較,大於當前結點則比較右子樹,小於當前結點則比較左子樹,等於則返回位置

以上步驟是靜態查找的步驟。當要插入數據或者刪除數據的時候,我們還需要實現插入結點和刪除結點同時調整二叉樹。

所以我們需要實現

  • 創建二叉樹——creatTree()
  • 查找二叉樹——binaryTreeSearch()
  • 插入結點——insertNode()
  • 刪除節點——deleteNode()

C++代碼實現

#include<cstdio>
#include<cstdlib> 

#define null  NULL

typedef struct Node{
	int value;
	struct Node *parent;
	struct Node *left;
	struct Node *right;
}treeNode;

//中序遍歷
void displayTree(treeNode* node){
        if(node == null) return;
 
        if(node->left != null){
                displayTree(node->left);
        }
 
        printf("%d ",node->value);
 
        if(node->right != null){
                displayTree(node->right);
        }
}
//插入結點 
void insertNode(treeNode *node,treeNode* inode){
	if(inode->value>=node->value&&node->right!=null){
		//如果大於等於該結點,而該結點右子樹不空則遞歸插入
		insertNode(node->right,inode);
		return; 
	} 
	if(inode->value<node->value&&node->left!=null){
		//如果小於該結點,而該結點左子樹不空則遞歸插入
		insertNode(node->left,inode);
		return; 
	} 
	if(inode->value>=node->value&&node->right==null){
		//如果大於該結點,而該結點右子樹空則插入
		node->right = inode;
		inode->parent = node;
	} 
	if(inode->value<node->value&&node->left==null){
		//如果大於該結點,而該結點右子樹空則插入
		node->left = inode;
		inode->parent = node;
	} 
} 

//創建二叉查找樹
void createBinaryTree(treeNode **root,int array[],int size){
	*root = (treeNode*)malloc(sizeof(treeNode));
	(*root)->value = array[0];//默認以第一個元素爲根結點。
	(*root)->right = null;
	(*root)->left = null;
	(*root)->parent = null;
	
	for(int i=1;i<size;i++){
		treeNode *child = (treeNode*)malloc(sizeof(treeNode));
		child->value=array[i];
		child->right =child->left= null;
		insertNode(*root,child);//插入結點 
	} 
	
} 
//查找value,成功則返回該結點 
treeNode* binaryTreeSearch(treeNode *node,int value){
	if(node->value==value)
		return node;
	else if(node->value>value){
		if(node->left!=null)
			return binaryTreeSearch(node->left,value);
		else return null;
	}else{
		if(node->right!=null)
			return binaryTreeSearch(node->right,value);
		else
			return null;
	}
}

//查找以node爲節點的樹中上是否存在vlaue的節點,parent爲查找到的節點的父節點。
//dir爲1表示parent節點的左節點爲查找結果;爲2表示parent節點的右節點爲查找結果
treeNode* searchTreeWithParent(treeNode* node, treeNode** parent, int* dir, int value){
        if(node->value == value){
                return node;
        }else if(node->value > value){
                if(node->left != NULL){
                        *dir = 1;
                        *parent = node;
                        return searchTreeWithParent(node->left, parent, dir, value);
                }else{
                        return NULL;
                }
        }else{
                if(node->right != NULL){
                        *dir = 2;
                        *parent = node;
                        return searchTreeWithParent(node->right, parent, dir, value);
                }else{
                        return NULL;
                }
        }
}


//從以root爲根節點的樹中刪除值爲value的節點
void deleteNode(treeNode** root, int value){
        treeNode* parent = NULL;
        int dir = -1;
        treeNode* deleteNode = searchTreeWithParent(*root,&parent,&dir,value);
        if(deleteNode == NULL){
                printf("%s\n", "node not found");
        }else{
                if(deleteNode->left == NULL && deleteNode->right == NULL){
                        if(parent != NULL){
                                if(dir == 1)
                                        parent->left = NULL;
                                else
                                        parent->right = NULL;
                        }else{
                                *root = NULL;
                        }
                }else if(deleteNode->left != NULL && deleteNode->right == NULL){
			if(parent != NULL){
                                if(dir == 1)
                                        parent->left = deleteNode->left;
                                else
                                        parent->right = deleteNode->left;
                        }else{
                                *root = deleteNode->left;
                        }
                }else if(deleteNode->left == NULL && deleteNode->right != NULL){
			if(parent != NULL){
                                if(dir == 1)
                                        parent->left = deleteNode->right;
                                else
                                        parent->right = deleteNode->right;
                        }else{
                                *root = deleteNode->right;
                        }
                }else{
                        insertNode(deleteNode->left,deleteNode->right);  
			if(parent != NULL){
                                if(dir == 1)
                                        parent->left = deleteNode->left;
                                else
                                        parent->right = deleteNode->left;
                        }else{
                                *root = deleteNode->left;
                        }
                }
                free(deleteNode);
                deleteNode = NULL;
        }
}
//刪除以node爲根結點的樹
void deleteTree(treeNode* node){
        if(node == NULL) return;
        if(node->left != NULL){
                deleteTree(node->left);
        }
        if(node->right != NULL){
                deleteTree(node->right);
        }
        if(node->left == NULL && node->right == NULL){
                free(node);
                node = NULL;
                }
}
int main(){
	int array[12] = {4,1,45,78,345,33,55,23,12,3,6,21};
 	//創建根結點 
    treeNode *root = NULL;
 	//創建二叉樹 
    createBinaryTree(&root, array, 12);
 
    printf("the tree is:");
    //打印構建的二叉樹 
    displayTree(root);
    printf("\n");
 
    int value = 78;
    printf("search value %d:",value);
    treeNode*objNode = null; 
    if((objNode=binaryTreeSearch(root,value))!= NULL){
            printf("%s\n","exist");
    }else{
            printf("%s\n","not exist");
    }
 
    printf("delete value:%d ",value);
    //刪除value這個值的結點 
    deleteNode(&root,value);
    printf("\n");
    printf("the tree is :");
    //重新打印二叉樹 
    displayTree(root);
    printf("\n");
 	//刪除整棵樹 
    deleteTree(root);
    return 0;

	
} 

在這裏插入圖片描述

4.5.2 平衡查找樹之2-3查找樹(2-3 Tree)

4.5.3 平衡查找樹之紅黑樹(Red-Black Tree)

4.5.4 B樹和B+樹(B Tree/B+ Tree)

4.6 哈希查找(哈希函數)

建立一個Hash表,查找表就是哈希表,如果是無衝突的哈希表,那麼能夠實現時間複雜度爲O(1)的查找。關於Hash表的描述,請參考HashTable詳解

4.7 分塊查找(改進的順序查找、折半查找)

屬於順序查找,分塊查找又稱索引順序查找,它是折半查找和順序查找的改進方法

  • 1、將數據分塊,使得分成的數據塊——塊內無序,塊間有序(第一塊的最大關鍵字必須小於第二塊的最小關鍵字,依次類推)
  • 2、 根據塊的最大關鍵字,構建索引表。圖解(圖是盜的)如下:在這裏插入圖片描述
  • 3、對索引表進行二分查找或者折半查找,得到要查找的數在哪一個塊,然後進入對應的塊裏邊順序查找即可。
    複雜度分析
    時間複雜度:O(log(m)+N/m)
    對比二分查找和折半查找
    在這裏插入圖片描述

java代碼:

/** 
 * @param index 索引表
 * @param obj 查找表
 * @param key 要查找的數
 * @param  每一塊的長度,假設相等 blocklen
 * @return 
 */ 
public static int blockSearch(int[] index, int[] obj, int key, int  blocklen) {   
    // 1.在index[ ] 中折半查找,確定要查找的key屬於哪個塊中  
    int i = binarySearch(index, key);  //查找索引表獲取數據在哪一塊
    if (i >= 0) {  
        int j = i* blocklen; //塊的起始索引
        int len = (i + 1) *  blocklen;  //結束索引
        // 在確定的塊中用順序查找方法查找key  
        for (; j < len; j++) {  
            if (key == st[j]) {  
                System.out.println("查詢成功");  
                return j;  
            }  
        }  
    }  
    System.out.println("查找失敗");  
    return -1;  
}  

參考博客

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章