需求
有一個數組,大小1000w,數據由小到大,現在用戶隨便輸入一個數字,如何快速的判斷此元素是否在數組中存在?
看到上面的需求你可能馬上就想到怎麼做了,循環這個數組,如果用戶輸入的數據等於當前循環的數據,表示存在,數組循環結束還沒有找到,代表數據不存在數組中,這種方法最簡單,循環一次數組就能得到結果,空間複雜度:O(1),時間複雜度:O(n),注意這裏的O(n)僅僅表示數組大小是變動的,如果恆定1000w,那麼時間複雜度:O(1)。
那有沒有更快的一種方式呢?讓時間複雜度降低一點呢?方法肯定是有的,就是今天要介紹的主角:二分查找。
什麼是二分查找
不知道你們以前玩過才數字遊戲沒有?出題方隨機選出一個數字,然後他會告訴你這個數字在一個區間內,讓你以最小的次數猜到數字是多少,每猜一次都會提示你所猜數字與給定數字的大小,比如:選中的數字:150,給定區間:1 -- 500,你有9次機會,如果猜對,獎勵電視機一臺。你們以前在超市有遇到過這種活動嗎?當時我就遇到過,精通各種數據結構以及算法的我(這句可以省略,明顯是吹牛),怎麼能受得了這種挑釁,當時就給超市老闆上了一課。
上面的這個例子我們就不能一個一個的猜,否者猜到9就沒然後了,那我們來看看二分是如何解決這個問題的?
二分查找也稱折半查找(Binary Search),它是一種效率較高的查找方法。但是,折半查找要求線性表必須採用順序存儲結構,而且表中元素按關鍵字有序排列。
看了二分介紹是不是覺得他正好能解決猜數字的遊戲呢?我們來還原一下當時是怎麼通過8次就猜對所選數字的:
1.選擇1--500中間的數,也就是250,由於選中的數字是150,所以出題方會告訴我猜的大了;
2.(250+1)/2=125,出題方提示猜小了;
3.(125+250)/2=187,出題方提示猜大了;
4.(125+187)/2=156,出題方提示猜大了;
5.(125=156)/2=140,出題方提示猜小了;
6.(140+156)/2=148,出題方提示猜小了;
7.(156+148)/2=152,出題方提示猜大了;
8.(148+152)/2=150,恭喜你答對了。
這就是今天要說的二分查找,現實生活中我們可以通過折半的方式找到對應的數字,程序中我們也能通過這樣的方式找到我們需要的數字,二分查找的理論很簡單,下面我們就通過代碼實現文章開頭的需求。
代碼實現二分查找
1.初始化數據
/**
* 初始化數據
* @param length 數組長度
* @return
*/
private static int[] initData(int length){
int[] arr = new int[length];
for(int i = 1; i<=length; ++i){
arr[i-1] = i;
}
return arr;
}
2.使用數組查找數據
/**
*使用二分查找數據
* @param a 數組內容
* @param n 數組長度
* @param value 要查詢的數字
* @return
*/
public static int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
count = 0;
while (low <= high) {
int mid = (low + high) / 2;
count++;
if (a[mid] == value) {
return mid;
} else if (a[mid] < value) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
3.定義一個成員變量:count(記錄查詢了多少次)
/**
* 一共搜索了多少次
*/
public static int count = 0;
4.main函數
public static void main(String[] args) {
int[] data = initData(500);
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("請輸入要查詢的數字:");
int num = scanner.nextInt();
if(num == -1){
System.exit(0);
}
int bsearch = bsearch(data, data.length, num);
System.out.println("搜索結果所在的下標:"+bsearch+",搜索了"+count+" 次");
}
}
5.運行
我隨機輸了5個數字,請看結果:
從結果來看,最多9次就能得出結果 ,是不是比循環整個數組快得多?這裏面的邏輯其實也很簡單,每次和取中的數據對比,然後得出下一個取中數據,繼續對比,直到找到數據或者取完還找不到數據返回-1。
如何找到數據在數組中第一次出現的位置?
之前說過了通過二分法可以快速的找到某個元素,但是怎麼做到找到這個元素第一次出現的位置呢?如數組a:{1,2,3,3,3,5,6};假設我們要查找元素3第一次出現的位置。該如何實現?還能繼續使用二分查找嗎?
我們分析一下,第一步使用3與數組中的a[3]對比,發現正好相等,直接返回了a[3]的下標(2),但這是元素3第一次出現的位置嗎?我們看數組a,第一次出現3的位置在下標:2,所以很明顯,通過二分查找的下標並不是我最終想得到的結果,那是不是意味着二分查找無法滿足了呢?
其實我們還是可以通過二分來實現此功能,只需要在二分的基礎上做一點點小改動就可以了,如何改動呢?我們想象一下,當我們使用3與數組中的a[3]對比的時候發現他們相等的同時判斷一下上一個元素是否也和他相等(a[2]),如果相等就繼續二分,否則返回當前下標,這樣是不是就能找到某個元素第一次出現的位置了呢?
下面我們使用代碼實現一下上面的需求
main函數:
public static void main(String[] args) {
int[] data = {1,2,3,3,3,5,6};
Scanner scanner = new Scanner(System.in);
while (true){
System.out.println("請輸入要查詢的數字:");
int num = scanner.nextInt();
if(num == -1){
System.exit(0);
}
int bsearch = bsearch(data, data.length, num);
System.out.println("第一次出現的下標:"+bsearch+",搜索了"+count+" 次");
}
}
二分查找:
/**
*使用二分查找數據
* @param a 數組內容
* @param n 數組長度
* @param value 要查詢的數字
* @return
*/
private static int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
count = 0;
while (low <= high) {
count++;
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
if ((mid == n - 1) || (a[mid - 1] != value)) return mid;
else low = mid + 1;
}
}
return -1;
}
仔細看,你會發現這與之前的代碼並沒有什麼差別,唯一一點變化在於判斷的時候多了一個條件:a[mid-1] != value,這樣就能實現找到某個元素在數組中第一次出現的位置,查找結果如下:
是不是很簡單?那我們再來看幾種情況。
查找最後一個值等於給定值的元素
相信大家都知道怎麼做了吧,和找到第一次出現的位置一樣,加一個判斷條件:當我們使用3與數組中的a[3]對比的時候發現他們相等的同時判斷一下下一個元素是否也和他相等(a[4]),如果相等就繼續二分,否則返回當前下標。
/**
*使用二分查找數據
* @param a 數組內容
* @param n 數組長度
* @param value 要查詢的數字
* @return
*/
private static int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
count = 0;
while (low <= high) {
count++;
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
if ((mid == n - 1) || (a[mid + 1] != value)) return mid;
else low = mid + 1;
}
}
return -1;
}
結果: