數據結構圖文解析之:二分查找及與其相關的幾個問題解析

0. 數據結構圖文解析系列

數據結構系列文章
數據結構圖文解析之:數組、單鏈表、雙鏈表介紹及C++模板實現
數據結構圖文解析之:棧的簡介及C++模板實現
數據結構圖文解析之:隊列詳解與C++模板實現
數據結構圖文解析之:樹的簡介及二叉排序樹C++模板實現.
數據結構圖文解析之:AVL樹詳解及C++模板實現
數據結構圖文解析之:二叉堆詳解及C++模板實現
數據結構圖文解析之:哈夫曼樹與哈夫曼編碼詳解及C++模板實現
數據結構圖文解析之:直接插入排序及其優化(二分插入排序)解析及C++實現
數據結構圖文解析之:二分查找及與其相關的幾個問題解析

1. 二分查找簡介

二分查找你們都不陌生,能夠說除了最簡單的順序查找以外,咱們第二個接觸的查找算法就是二分查找了。順序查找的時間複雜度是O(n),二分查找的時間複雜度爲O(logn)。在面試中二分查找被考察的機率仍是比較高的,上次去面試時就遇到手寫二分查找的題目。二分查找不難,但咱們要能作到準確、快速地寫出二分查找的代碼,能對二分查找的效率作出分析,還可以將二分查找的思想來解決其餘的問題。html

1.1 二分查找過程圖解

二分查找要求序列自己是有序的。所以對於無序的序列,咱們要先對其進行排序。如今咱們手頭有一有序序列:
array[10] = {2,4,5,7,,8,9,13,23,34,45},則二分查找的過程爲:面試

  • 設置三個索引:start指向數組待查範圍的起始元素,end指向數組待查範圍的最後一個元素,middle=(start+end)/2。開始時待查範圍爲整個數組。
  • 比較array[middle]與查找元素的大小關係:
    • 若是array[middle]等於查找元素,則查找成功
    • 若是array[middle]大於查找元素,則說明待查元素在數組的前半部分,此時縮小待查範圍,令end = middle-1
    • 若是array[middle]小於查找元素,則說明待查元素在數組的後半部分,此時縮小待查範圍,令start = middle +1
  • 重複執行前面兩步,直到array[middle ] 等於查找元素則查找成功或start>end查找失敗。

如今咱們在數組{2,4,5,7,,8,9,13,23,34,45}中查找元素23,過程如圖:算法

可見,每一次元素比較均可以把待查範圍縮小1/2,所以二分查找的時間複雜度爲o(logn)。數組

1.2 二分查找實現代碼

二分查找代碼簡單,能夠遞歸或迭代地實現。遞歸容易實現,代碼簡單,但待查元素數量巨大時,遞歸深度也隨之增大(logn的關係),應考慮是否會發生棧溢出。迭代的實現也不復雜,但咱們力求準確簡潔。數據結構

遞歸實現

//查找成功時返回查找元素在數組中的下標;查找失敗時返回-1
template <typename T>
int BinarySearch(const T array[], int start, int end, const T& value)
{
    if (start>end)
        return -1;
    int middle = (start + end) / 2;
    if (array[middle] == value)
        return middle;
    if (array[middle] < value)
    {
        return BinarySearch(array, middle+1, end, value);
    }
    return BinarySearch(array, start, middle-1, value);
};

迭代實現

template <typename T>
int BinarySearch(const T array[], int start, int end, const T& value)
{
    int result = -1;
    while (start <= end)
    {
        int middle = (start + end) / 2;
        if (array[middle] == value)
        {
            result = middle;
            break;
        }
        if (array[middle] < value)
        {
            start = middle + 1;
        }
        else
            end = middle - 1;
    }
    return result;
}

測試

爲了方便用戶使用,咱們定義一個接口:函數

//用戶使用接口
template <typename T>
int BinarySearch(const T array[], int length, const T& value)
{
    if (array == nullptr || length <= 0)
        return -1;
    return BinarySearch(array, 0, length - 1, value);
}

使用實例:測試

int _tmain(int argc, _TCHAR* argv[])
{
    int array[10] = { 2, 4, 5, 7, 8, 9, 13, 23, 34, 45 };
    int i;
    while (cin >> i)
    {
        cout << BinarySearch(array, 10, i) << endl;
    }
    return 0;
}

2. 二分查找的應用

2.1 二分插入排序

這是對直接插入排序的一種優化策略,可以有效減小插入排序的比較次數。
直接插入排序的思路是對於無序序列的第一個元素,從後至前進行順序查找 掃描有序序列尋找合適的插入點。改進後的二分插入排序算法使用二分查找在有序序列中查找插入點,將插入排序的比較次數降爲O(log2n)。這個思路的實現代碼爲:優化

/*二分查找函數,返回插入下標*/
template <typename T>
int BinarySearch(T array[], int start, int end, T k)
{
    while (start <= end)
    {
        int middle = (start + end) / 2;
        int middleData = array[middle];
        if (middleData > k)
        {
            end = middle - 1;
        }
        else
            start = middle + 1;
    }
    return start;
}
//二叉查找插入排序
template <typename T>
void InsertSort(T array[], int length)
{
    if (array == nullptr || length < 0)
        return;
    int i, j;
    for (i = 1; i < length; i++)
    {
        if (array[i]<array[i - 1])
        {
            int temp = array[i];
            int insertIndex = BinarySearch(array, 0,i, array[i]);//使用二分查找在有序序列中進行查找,獲取插入下標
            for (j = i - 1; j>=insertIndex; j--) //移動元素
            {
                array[j + 1] = array[j];  
            }      
            array[insertIndex] = temp;    //插入元素
        }
    }
}

若對插入排序不瞭解,能夠看數據結構圖文解析之:直接插入排序及其優化(二分插入排序)解析及C++實現編碼

2.2 旋轉數組的最小數字問題

問題描述:把一個數組最開始的若干個元素搬到數組的末尾,咱們稱爲數組的旋轉,輸入一個遞增排序數組的一個旋轉,輸出旋轉數組的最小元素。例如數組{4,5,6,1,2,3}爲數組 {1,2,3,4,5,6}的一個旋轉,最小旋轉元素爲1.3d

解決思路:
尋找數組中最小的元素,咱們能夠遍歷數組,時間複雜度爲O(n)。可是藉助二分查找的思想,咱們可以在O(logn)的時間複雜度內找到最小的元素。旋轉數組的特色是數組中兩個部分都分別是有序的,以{4,5,6,1,2,3}爲例,{4,5,6}是非遞減的,{1,2,3}也是非遞減的:

咱們能夠定義兩個索引:start指向第一部分的起始位置;end指向第二部分的最後一個元素,如圖所示。因爲數組在旋轉前總體有序,故array[start]>=array[end],而中間值array[middle]知足:

  • 若是array[middle]>array[start] ,則array[middle]屬於第一部分。此時令start= middle ;
  • 若是array[middle]<array[start] ,則array[middle]屬於第二部分。此時令end = middle ;

循環執行上述操做,當start與end相鄰時,end所指即是數組中的最小元素:

end所指元素即是最小元素。
這個尋址的過程有一個隱喻的要求:中間這個元素必須可以判斷它是屬於第一部分仍是第二部分。在有些輸入下,這個要求不能知足,例如數組:{0,1,1,1,1},它的兩個選擇數組爲:

  • {1,0,1,1,1}
  • {1,1,1,0,1}

此時因array[middle] == array[start] == array[end] 而沒法判斷array[middle]屬於哪一部分,咱們只能進行順序查找找出最小元素。

如圖:

此時沒法肯定array[middle]是屬於第一部分仍是第二部分,這種狀況下咱們須要進行順序查找。
所以,咱們的代碼實現爲:

//順序查找函數
int Min(int array[], int length)
{
    int result = array[0];
    for (int i = 1; i < length; i++)
    {
        if (array[i] < result)
            result = array[i];
    }
    return result;
}
 
int MinInRotation(int array[],int length)
{
    int result = -999;
    if (array == nullptr || length < 0)
        return result;
    int start = 0;
    int end = length - 1;
    int middle = start;
    while (start < end)
    {
        if (start + 1 == end)
        {
            result = array[end];
            break;
        }
        middle = (start + end)/2;
        //若是趕上特殊狀況,則須要進行順序查找
        if (array[middle] == array[start] && array[middle] == array[end])
            return Min(array,length); //調用順序查找函數
        //不然;中間元素屬於第一部分
        if (array[middle]>=array[start])
        {
            start = middle;
        }//中間元素屬於第二部分
        else if (array[middle] <= array[start])
        {
            end = middle;
        }
    }
    return result;
}

測試代碼:

int _tmain(int argc, _TCHAR* argv[])
{
    int array1[6] = { 4, 5, 6, 1, 2, 3 };
    int array2[5] = { 1, 0, 1, 1, 1 };
    int array3[5] = { 1, 1, 1, 0, 1 };
    cout << MinInRotation(array1, 6)<<endl;
    cout << MinInRotation(array2, 5) << endl;
    cout << MinInRotation(array3, 5) << endl;
 
    getchar();
    return 0;
}

運行結果:

1
0
0

2.3 數字在排序數組中出現次數問題

問題描述:統計一個數組在排序數組中出現的次數。例如輸入排序數組{1,2,3,3,3,3,4,5}和數字3,因爲3在數組中出現了4次,所以輸出4.

解決思路:假設咱們是統計數字k在排序數組中出現的次數,只要找出排序數組中第一個k與最後一個k的下標,就可以計算出k的出現次數。
尋找第一個k時,利用二分查找的思想,咱們老是拿k與數組的中間元素進行比較。若是中間元素比k大,那麼第一個k只有可能出如今數組的前半段;若是中間元素等於k,咱們就須要判斷k是不是前半段的第一個k,若是k前面的元素不等於k,那麼說明這是第一個k;若是k前面的元素依舊是k,那麼說明第一個k在數組的前半段中,咱們要繼續遞歸查找。

這個過程的代碼:

int getFirstIndex(int array[], int start ,int end, int k)
{
    if (start > end)
        return -1;
    int middle = (start + end) / 2;
    int middleData = array[middle];
    if (middleData == k)
    {
        if (middle == 0 || array[middle - 1] != k)
            return middle;
        else
            end = middle - 1;
    }
    else if (middleData>k)
    {
        end = middle - 1;
    }
    else
        start = middle + 1;
    return getFirstIndex(array, start, end, k);
}

一樣的思路,咱們在數組中尋找最後一個k,若是中間元素比K大,那麼k只能出如今數組的後半段;若是中間元素比K小,那麼K只能出如今數組的前半段。若是中間元素等於k,而k後面的元素等於k,那麼最後一個k只能在後半段出現;不然k爲數組中最後的一個k。

代碼以下:

///獲取最後一個K的下標
int getLastIndex(int array[], int start,int end,int length, int k)
{
    if (start > end)
        return -1;
    int middle = (start + end) / 2;
    int middleData = array[middle];
    if (middleData == k)
    {
        if (middle == length-1 || array[middle+ 1] != k) //最後一個k
            return middle;
        else
            start = middle + 1;
    }
    else if (middleData>k)
    {
        end = middle - 1;
    }
    else
        start = middle + 1;
    return getLastIndex(array, start, end,length, k);
}

把第一個座標與最後一個座標算出來後,咱們能夠進行元素出現次數的計算:

//計算元素出現個數
int CountKInArray(int array[], int length, int k)
{
    int result=-1;
    if (array != nullptr && length > 0)
    {
        int firstPos = getFirstIndex(array, 0, length - 1, k);
        int lastPos = getLastIndex(array, 0, length - 1, length, k);
        if (firstPos != -1 && lastPos != -1)
        {
            result = lastPos - firstPos+1;
        }
    }
    return result;
}

測試:

int _tmain(int argc, _TCHAR* argv[])
{
    int array[8] = { 1, 2, 3, 3, 3, 4, 5 };
 
    cout << "數組array中數字3出現的次數爲:"<<CountKInArray(array, 7, 3) << endl;
    getchar();
    return 0;
}

運行結果:

數組array中數字3出現的次數爲:3

原創文章,轉載請註明:http://www.cnblogs.com/QG-whz/p/5194627.html

相關文章
相關標籤/搜索