簡體   English   中英

在 O(lg n) 中查找 Python 列表的唯一數字對中的單個數字

[英]Find single number in pairs of unique numbers of a Python list in O(lg n)

我對編程算法中的分而治之有疑問。 假設您在 Python 中獲得一個隨機 integer 列表,其中包括:

  1. 唯一的連續整數對
  2. 列表中某處的單個 integer

並且條件是排他性的,這意味着雖然[2,2,1,1,3,3,4,5,5,6,6]是有效的,但這些不是:

  1. [2,2,2,2,3,3,4] (違反條件1:因為有兩對2而最多只能有一對任意數)
  2. [1,4,4,5,5,6,6,1] (違反條件 1:因為有一對 1 但它們不連續)。
  3. [1,4,4,5,5,6,6,3] (違反條件2:有2個單數,1和3)

現在的問題是你能在 O(lgn) 算法中找到“單個”數字索引嗎?

我原來的刺拳是這樣的:

def single_num(array, arr_max_len):

  i = 0

  while (i < arr_max_len):
    if (arr_max_len - i == 1):
      return i
    elif (array[i] == array[i + 1]):
      i = i + 2
    else:
      return i # don't have to worry about odd index because it will never happen
  
  return None 

然而,該算法似乎在 O(n/2) 時間運行,這似乎是它可以做到的最好的。

即使我使用分而治之,我認為它不會比 O(n/2) 時間更好,除非有一些方法超出了我目前 scope 的理解范圍。

任何人都有更好的主意,或者我可以說,這已經是 O(log n) 時間了?

編輯:曼努埃爾似乎有最好的解決方案,如果允許的話,我將有時間自己實施解決方案以供理解,然后接受曼努埃爾的回答。

解決方案

只需對偶數索引進行二進制搜索即可找到值與下一個值不同的第一個索引。

from bisect import bisect

def single_num(a):
    class E:
        def __getitem__(_, i):
            return a[2*i] != a[2*i+1]
    return 2 * bisect(E(), False, 0, len(a)//2)

解釋

我正在搜索的虛擬“列表” E()的可視化:

       0  1   2  3   4  5   6  7   8  9   10 (indices)
  a = [2, 2,  1, 1,  3, 3,  4, 5,  5, 6,  6]
E() = [False, False, False, True,  True]
       0      1      2      3      4     (indices)

一開始,這對匹配(所以!=導致False值)。 從單個數字開始,對匹配(因此!=返回True )。 由於False < True ,這是一個bisect愉快地搜索的排序列表。

替代實施

如果沒有bisect ,如果您還沒有厭倦編寫二進制搜索:

def single_num(a):
    i, j = 0, len(a) // 2
    while i < j:
        m = (i + j) // 2
        if a[2*m] == a[2*m+1]:
            i = m + 1
        else:
            j = m
    return 2*i

嘆...

我希望bisect支持給它一個可調用的,所以我可以做return 2 * bisect(lambda i: a[2*i],= a[2*i+1], False, 0, len(a)//2) . Ruby 確實如此,這可能是我有時使用 Ruby 而不是 Python 解決編碼問題的最常見原因。

測試

順便說一句,我用所有可能的情況對最多 1000 對進行了測試:

from random import random

for pairs in range(1001):
    a = [x for _ in range(pairs) for x in [random()] * 2]
    single = random()
    assert len(set(a)) == pairs and single not in a
    for i in range(0, 2*pairs+1, 2):
        a.insert(i, single)
        assert single_num(a) == i
        a.pop(i)

lg n 算法是將輸入分成更小的部分,並丟棄一些更小的部分,這樣你就有更小的輸入可以使用。 由於這是一個搜索問題,因此 lg n 時間復雜度的可能解決方案是二進制搜索,其中您每次將輸入分成兩半。


我的方法是從幾個簡單的案例開始,找出我可以利用的任何模式。

在以下示例中,最大的 integer 是目標編號。

# input size: 3  
[1,1,2]
[2,1,1]

# input size: 5  
[1,1,2,2,3]
[1,1,3,2,2]
[3,1,1,2,2]

# input size: 7  
[1,1,2,2,3,3,4]
[1,1,2,2,4,3,3]
[1,1,4,2,2,3,3]
[4,1,1,2,2,3,3]

# input size: 9  
[1,1,2,2,3,3,4,4,5]
[1,1,2,2,3,3,5,4,4]
[1,1,2,2,5,3,3,4,4]
[1,1,5,2,2,3,3,4,4]
[5,1,1,2,2,3,3,4,4]

您可能注意到輸入大小始終是奇數,即2*x + 1

由於這是一個二分搜索,您可以檢查中間數字是否是您的目標數字。 如果中間數字是單個數字( if middle_number != left_number and middle_number != right_number ),那么您已經找到它。 否則,您必須搜索輸入的左側或右側。

請注意,在上面的示例測試用例中,中間數字不是目標數字,中間數字及其對之間存在模式。

對於輸入大小 3 (2*1 + 1), if middle_number == left_number ,則目標數字在右側,反之亦然。

對於輸入大小 5 (2*2 + 1), if middle_number == left_number ,則目標數字在左側,反之亦然。

對於輸入大小 7 (2*3 + 1), if middle_number == left_number ,則目標數字在右側,反之亦然。

對於輸入大小 9 (2*4 + 1), if middle_number == left_number ,則目標數字在左側,反之亦然。

這意味着 x 在2*x + 1 (數組長度)中的奇偶性影響是搜索輸入的左側還是右側:如果 x 為奇數則搜索右側,如果 x 為偶數則搜索左側,如果 middle_number == left_number(反之亦然)。


基於所有這些信息,您可以提出遞歸解決方案。 請注意,您必須確保每個遞歸調用中的輸入大小都是奇數。 (編輯:確保輸入大小是奇數會使代碼更加混亂。您可能想提出一個解決方案,其中輸入大小的奇偶性無關緊要。)

def find_single_number(array: list, start_index: int, end_index: int):
    # base case: array length == 1
    if start_index == end_index:
        return start_index
    
    middle_index = (start_index + end_index) // 2
        
    # base case: found target
    if array[middle_index] != array[middle_index - 1] and array[middle_index] != array[middle_index + 1]:
        return middle_index
        
    # make use of parity of array length to search left or right side
    # end_index == array length - 1
    x = (end_index - start_index) // 2

    # ensure array length is odd
    include_middle = (middle_index % 2 == 0)
        
    if array[middle_index] == array[middle_index - 1]:  # middle == number on its left
        if x % 2 == 0:  # x is even
            # search left side
            return find_single_number(
                array,
                start_index,
                middle_index if include_middle else middle_index - 1
            )

        else:  # x is odd
            # search right side side
            return find_single_number(
                array,
                middle_index if include_middle else middle_index + 1,
                end_index,
            )

    else:  # middle == number on its right
        if x % 2 == 0:  # x is even
            # search right side side
            return find_single_number(
                array,
                middle_index if include_middle else middle_index + 1,
                end_index,
            )

        else:  # x is odd
            # search left side
            return find_single_number(
                array,
                start_index,
                middle_index if include_middle else middle_index - 1
            )


# test out the code
if __name__ == '__main__':
    array = [2,2,1,1,3,3,4,5,5,6,6]  # target: 4 (index: 6)
    print(find_single_number(array, 0, len(array) - 1))

    array = [1,1,2]  # target: 2 (index: 2)
    print(find_single_number(array, 0, len(array) - 1))

    array = [1,1,3,2,2]  # target: 3 (index: 2)
    print(find_single_number(array, 0, len(array) - 1))

    array = [1,1,4,2,2,3,3]  # target: 4 (index: 2)
    print(find_single_number(array, 0, len(array) - 1))

    array = [5,1,1,2,2,3,3,4,4]  # target: 5 (index:0)
    print(find_single_number(array, 0, len(array) - 1))

我的解決方案可能不是最有效或最優雅的,但我希望我的解釋能幫助您理解解決這類算法問題的方法。


證明它的時間復雜度為 O(lg n):

假設最重要的操作是中間數與左右數的比較( if array[middle_index] != array[middle_index - 1] and array[middle_index] != array[middle_index + 1] ),並且它的時間成本為 1 個單位。 讓我們將此比較稱為主要比較。

令 T 為算法的時間成本。
設 n 為數組的長度。

由於此解決方案涉及遞歸,因此存在基本情況和遞歸情況。

對於基本情況(n = 1),這只是主要比較,所以:
T(1) = 1。

對於遞歸情況,每次將輸入分成兩半(左半部分或右半部分); 同時,還有一個主要的比較。 所以:
T(n) = T(n/2) + 1

現在,我知道輸入大小必須始終為奇數,但為了簡單起見,讓我們假設 n = 2 k 時間復雜度仍然相同。

我們可以將 T(n) = T(n/2) + 1 重寫為:
T(2 k ) = T(2 k-1 ) + 1

此外,T(1) = 1 是: T(2 0 ) = 1

當我們展開 T(2 k ) = T(2 k-1 ) + 1 時,我們得到:

T( 2k )
= T(2 k-1 ) + 1
= [T(2 k-2 ) + 1] + 1 = T(2 k-2 ) + 2
= [T(2 k-3 ) + 1] + 2 = T(2 k-3 ) + 3
= [T(2 k-4 ) + 1] + 3 = T(2 k-4 ) + 4
=...(重復直到 k)
= T(2 kk ) + k = T(2 0 ) + k = k + 1

由於 n = 2 k ,這意味着 k = log 2 n。

將 n 代入,我們得到: T(n) = log 2 n + 1

1 是一個常數,所以可以去掉; 日志操作的基礎也是如此。

因此,算法時間復雜度的上界為:
T(n) = lg n

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM