二分查找應用 --- 有序數組中的單一元素

前言

大家好,我是程序員小熊,來自大廠的程序猿。瞭解二分查找的童鞋,都知道二分查找常用於在有序數組中查找某一特定元素,而且很多童鞋也都知道二分查找的模板該怎麼寫。

今天小熊帶來一道亞馬遜的面試題,也就是力扣 540. 有序數組中的單一元素,這道題難度爲中等,採用 “二分查找 + 動圖” 的方式深入剖析,供大家參考,希望對大家有所幫助。

題目

給定一個只包含整數的有序數組,每個元素都會出現兩次,唯有一個數只會出現一次,找出這個數。

示例 1:
輸入: [1,1,2,3,3,4,4,8,8]
輸出: 2

示例 2:
輸入: [3,3,7,7,10,11,11]
輸出: 10

注意: 您的方案應該在 O(log n)時間複雜度
      和 O(1)空間複雜度中運行。

解題思路

本題如果不要求解法的時間複雜度爲 O(log n) 的話,就跟力扣 136. 只出現一次的數字差不多,只是後者不要求數組是有序的,但解法一樣,可以通過異或去做,因爲一個數字跟自身相異或,結果爲 0;但異或 0,結果爲自身,因此讓數組中所有元素都相互異或即可得到結果,但時間複雜度爲 O(n)

由於題目明確要求解法的時間複雜度爲 O(log n),對二分查找有所瞭解的童鞋,很自然地會想到需要採用二分查找法去做。

那具體如何通過二分查找去做呢?見下面例子。

舉例

以數組 [3,3,7,7,10,11,11] 爲例子,如下圖示。

示例

二分查找一般通過數組的中間元素 nums[mid] 判斷 target 的位置(在 mid 位置,亦或是在 mid 的左側或右側),本題也不例外。

確定中間元素

由題意可知,數組長度一定爲奇數,因此可以進行如下操作:

  1. 1、判斷中間元素是否跟兩側元素相等

  2. 2、若等於任意一側元素,則去掉中間元素及其跟它相等的元素,將原數組分爲兩部分(奇數長度和偶數長度),由於唯一的那個數一定存在於奇數長度的數組,因此丟棄偶數長度的子數組,在奇數長度的子數組中重複 1 和 2;

  3. 3、若不等於兩側元素,則中間元素就是要查找的只出現一次的那個數字

如下圖示:

1、判斷 nums[mid] == nums[mid - 1];

2、移除 nums[mid] 和 nums[mid - 1];

3、判斷拆分後的兩數組的長度,並移除偶數長度子數組;

4、在奇數長度的子數組中重複前 1、2、3 步;

查找過程完整動態展示

動態如下:

Show me the Code

C

int singleNonDuplicate(int* nums, int numsSize){
    int left = 0, right = numsSize - 1;
    while (left < right) {
        int mid = left + ((right - left) >> 1);
        /* 判斷(子)數組的長度,進而確認 target 位置 */
        bool halvesAreEven = (right - mid) % 2 == 0;
        /* 中間元素等於右邊元素 */
        if (nums[mid + 1] == nums[mid]) {
            /* halvesAreEven 爲偶數,mid 右側存在 target */
            if (halvesAreEven) {
                left = mid + 2;
            /* halvesAreEven 爲奇數,mid 左側存在 target */
            } else {
                right = mid - 1;
            }
        /* 中間元素等於左側元素 */
        } else if (nums[mid - 1] == nums[mid]) {
            /* halvesAreEven 爲偶數,mid 左側存在 target */
            if (halvesAreEven) {
                right = mid - 2;
            /* halvesAreEven 爲奇數,mid 右側存在 target */
            } else {
                left = mid + 1;
            }
        /* 中間元素跟左右側元素都不等,中間元素是target */    
        } else {
            return nums[mid];
        }
    }
    return nums[left];
}

簡化

上面的代碼略顯冗餘,if 跟 else if 中均需要先判斷 nums[mid] 與兩側的元素是否相等,再判斷 halvesAreEven 是否爲奇數,然後決定在 mid 的左側還是右側查找,有沒有簡便一點的方法?

答案是有的。

  1. 1、將 nums[mid + 1]、nums[mid - 1] 與 nums[mid] 比較,簡化爲 nums[temp] 與 nums[mid] 比較,其中 temp = mid ^ 1,若相等則左邊界 left 右移(left = mid + 1)繼續查找;

  2. 2、若不相等,則相當於 nums[mid] 與左右兩側元素都不相等,相當於 mid 就是待查找的只出現一次的那個數字。

舉例

以數組 [1,1,2,3,3,4,4] 爲例子,如下動圖示。

示例動圖

Show me the Code

C++

int singleNonDuplicate(vector<int>& nums) {
    int left = 0, right = nums.size() - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        int temp = mid ^ 1;  
        if (nums[mid] == nums[temp]) {
            left = mid + 1;
        } else {
            right = mid;
        }
    }

    return nums[left];
}

Golang

func singleNonDuplicate(nums []int) int {
    left, right := 0, len(nums) - 1
    for left < right { 
        mid := left + (right - left) / 2;
        temp := mid ^ 1; 

        if nums[mid] == nums[temp] {
            left = mid + 1;
        } else {
            right = mid;
        }        
    }

    return nums[left]
}

Python3

def singleNonDuplicate(self, nums: List[int]) -> int:
    left, right = 0, len(nums) - 1
    while left < right:
        mid = (right + left) // 2;
        temp = mid ^ 1; 

        if nums[mid] == nums[temp]:
            left = mid + 1
        else:
            right = mid

    return nums[left]

複雜度分析

空間複雜度O(1),未開闢額外存儲空間。

時間複雜度O(logn),n 爲數組長度。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/w-6W9z7Ura7dayMzR0LEVg