python code practice(一): 快排、歸並、堆排的實現,雙指針/滑動窗口技術


基本原理就不做介紹了, 很基礎的數據結構課程知識.私下回顧即可,主要學習代碼.

1.雙指針

https://leetcode-cn.com/problems/two-sum-ii-input-array-is-sorted/description/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china

題目:

167. 兩數之和 II - 輸入有序數組

給定一個已按照升序排列有序數組,找到兩個數使得它們相加之和等於目標數。

函數應該返回這兩個下標值 index1 和 index2,其中 index1 必須小於 index2。

說明:

返回的下標值(index1 和 index2)不是從零開始的。
你可以假設每個輸入只對應唯一的答案,而且你不可以重復使用相同的元素。
示例:

輸入: numbers = [2, 7, 11, 15], target = 9
輸出: [1,2]
解釋: 2 與 7 之和等於目標數 9 。因此 index1 = 1, index2 = 2 。

分析:

利用排序數組的性質,采用雙指針來在時間O(n)和空間O(1)解決。

1.特殊判定, 若數組為空,返回[]

2.定義左指針L=0指向最小元素,定義右指針R=n-1指向最大元素

3.循環條件: L<R

  若number[L] + number[R] == target, 則返回[L+1, R+1]

  若number[L] + number[R] > target, 說明右邊的值太大,調小一點,即R=R-1

  若number[L] + number[R] < target, 說明左邊的值太小,調大一點,即L= L+1

4.執行到這一步,若不返回,說明無解.返回[-1, -1]

復雜度分析:

時間復雜度:O(n)

空間復雜度:O(l)

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        if(not numbers):
            return []
        # res=[]
        n=len(numbers)
        l=0
        r=n-1
        while(l<r):
            if(numbers[l]+numbers[r]==target):
                return [l+1,r+1]
            elif(numbers[l]+numbers[r]>target):
                r=r-1
            else:
                l=l+1
        return [-1,-1]

2.快速選擇,堆排序,歸並排序

https://leetcode-cn.com/problems/kth-largest-element-in-an-array/description/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china

題目:

215題: 數組中的第K個最大元素

在未排序的數組中找到第K個最大元素.請注意,你需要找的是數組排序后的第K個最大的元素,而不是第K個不同的元素.

示例 1:

輸入: [3,2,1,5,6,4] 和 k = 2
輸出: 5
示例 2:

輸入: [3,2,3,1,2,4,5,5,6] 和 k = 4
輸出: 4
說明:

你可以假設 k 總是有效的,且 1 ≤ k ≤ 數組的長度。

分析:

方法0:排序

最朴素的方法是先對數組進行排序,再返回倒數第K個元素,就像python中的sorted(nums)[-k]

算法的時間復雜度為O(NlogN),空間復雜度為O(1).這個時間復雜度並不令人滿意,試着用額外空間來優化時間復雜度.

方法1:堆

創建一個小頂堆,將所有數組中的元素加入堆中,並保持堆的大小<=k.這樣堆中就保留了前k個最大元素.此時,堆頂的元素就是正確答案.

向大小為k的堆中添加元素的時間復雜度是O(logk),重復該操作N次,總時間復雜度為O(Nlogk).

python的heapq庫中有一個nlargest方法,具有同樣的時間復雜度,能將代碼簡化到一行.

本方法優化了時間復雜度,但需要O(k)的空間復雜度.

class Solution:
    def findKthLargest(self, nums, k):
        """
        :type nums: List[int]
        :type k: int
        :rtype: int
        """
        return heapq.nlargest(k, nums)[-1]

復雜度分析:

時間復雜度 :O(Nlogk)。
空間復雜度 : O(k),用於存儲堆元素。

方法2:快速選擇

這種方法應該叫快速選擇(Quick Select),跟快排的思想是非常接近的:選取一個基准元素pivot,將數組切分(partition)為兩個子數組,比pivot大的扔左子數組,比pivot小的扔右子數組,然后遞推地切分子數組。Quick Select不同於Quick Sort的是其沒有對每個子數組做切分,而是對目標子數組做切分。

快速選擇算法 的平均時間復雜度為O(N)。就像快速排序那樣.

注意到第k個最大元素也是第n-k+1個最小元素,因此可以用第k小算法來解決本問題.

首先,我們選擇一個樞軸,並在線性時間內定義其在排序數組中的位置。這可以通過 划分算法 的幫助來完成。

為了實現划分,沿着數組移動,將每個元素與樞軸進行比較,並將小於樞軸的所有元素移動到樞軸的左側。

這樣,在輸出的數組中,樞軸達到其合適位置。所有小於樞軸的元素都在其左側,所有大於或等於的元素都在其右側。

這樣,數組就被分成了兩部分。如果是快速排序算法,會在這里遞歸地對兩部分進行快速排序,時間復雜度為O(NlogN)。

由於知道要找的第 N - k + 1小的元素在哪部分中,我們不需要對兩部分都做處理,這樣就將平均時間復雜度下降到 O(N)。

最終的算法十分直截了當 :

  隨機選擇一個樞軸。

  使用划分算法將樞軸放在數組中的合適位置 pos。將小於樞軸的元素移到左邊,大於等於樞軸的元素移到右邊。

  比較 pos 和 N - k +1以決定在哪邊繼續遞歸處理。

注意,本算法也適用於有重復的數組

 

 
         
"""
Quick Select的目標是找出第k大元素,所以:

若切分后的左子數組的長度 > k,則第k大元素必出現在左子數組中;
若切分后的左子數組的長度 = k - 1,則第k大元素為pivot;
若上述兩個條件均不滿足,則第k大元素必出現在右子數組中。
基於上述,則可以很容易的得出Quick Select的實現代碼:

"""

class
Solution: def findKthLargest(self, nums, k): """ :type nums: List[int] :type k: int :rtype: int """ def partition(left, right, pivot_index): pivot = nums[pivot_index] # 1. move pivot to end nums[pivot_index], nums[right] = nums[right], nums[pivot_index] # 2. move all smaller elements to the left store_index = left for i in range(left, right): if nums[i] < pivot: nums[store_index], nums[i] = nums[i], nums[store_index] store_index += 1 # 3. move pivot to its final place nums[right], nums[store_index] = nums[store_index], nums[right] return store_index def select(left, right, k_smallest): """ Returns the k-th smallest element of list within left..right """ if left == right: # If the list contains only one element, return nums[left] # return that element # select a random pivot_index between pivot_index = random.randint(left, right) # find the pivot position in a sorted list pivot_index = partition(left, right, pivot_index) # the pivot is in its final sorted position if k_smallest == pivot_index: return nums[k_smallest] # go left elif k_smallest < pivot_index: return select(left, pivot_index - 1, k_smallest) # go right else: return select(pivot_index + 1, right, k_smallest) # kth largest is (n - k)th smallest return select(0, len(nums) - 1, len(nums) - k)

復雜度分析

  • 時間復雜度 : 平均情況 O(N),最壞情況 O(N2)。
  • 空間復雜度 : O(1)。

3.桶排序

https://leetcode-cn.com/problems/top-k-frequent-elements/description/?utm_source=LCUS&utm_medium=ip_redirect_q_uns&utm_campaign=transfer2china

題目:

347題: 前K個高頻元素

給定一個非空的整數數組,返回其中出現頻率前 k 高的元素。

示例 1:

輸入: nums = [1,1,1,2,2,3], k = 2
輸出: [1,2]
示例 2:

輸入: nums = [1], k = 1
輸出: [1]
說明:

你可以假設給定的 k 總是合理的,且 1 ≤ k ≤ 數組中不相同的元素的個數。
你的算法的時間復雜度必須優於 O(n log n) , n 是數組的大小。

方法1:使用Counter

思路:

統計每個數出現的頻率,輸出最大的幾個,這完全迎合了Python中的Counter類,調用其的幾個方法即可。

什么是Counter?

Counter 是一個在collections包里的類,正如其名,是一個用於計數的工具。
我們可以用Counter(nums)這樣的構造函數構造一個Counter類,其中nums是一個列表。
構造好的Counter實例可以看作一個字典,鍵是nums的每一項,值是它的出現次數。
如果上面的敘述讓你感到很混亂的話,我不妨舉個例子。
如果一個列表a = [1,1,3,4,3],你想要統計每項的出現次數,那么你使用b = Counter(a),那么這時候b就像一個這樣的字典{1:2,3:2,4:1},表示數字1出現了2次,數字3出現了2次,數字4出現了1次。
還是很好理解的吧?
可是題目里要我們輸出的是最多的K項
這時候可以應用Counter的一個函數,most_common(k)
這個函數就是返回最多出現的K項
但是返回的形式是一個元祖列表,類似[(1,2),(3,2),(4,1)]的形式
我們只需要鍵也就是第一項,所以要使用列表生成式處理一下即可。

from collections import Counter
class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        return [i[0] for i in Counter(nums).most_common(k)]

方法2:桶排序

桶排序科普:https://www.cnblogs.com/bqwzx/p/11029264.html

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        map = {}
        for i in nums:
            map[i] = map.get(i,0) + 1    #生成字典映射
        max_time = max(map.values())
        TongList = [[] for i in range(max_time+1)] #根據最大次數生成桶
        for key, value in map.items():
            TongList[value].append(key) #將索引value放入key對應的字典索引
        res = []
        for i in range(max_time, 0, -1): #按桶索引排序
            if TongList[i]:
                res.extend(TongList[i])
            if len(res) >= k:
                return res[:k]

方法3:

第一步:hash統計次數(在Python中要巧用字典)
第二步:字典鍵值對壓入大頂堆(注意如何將heapq小頂堆作為大頂堆使用)
第三步:大頂堆吐出k個值加入返回list

import heapq


class Solution:
    def topKFrequent(self, nums, k):
        heap_max = [] //初始化一個大頂堆
        dic_fre = {}  //初始化一個字典
        ans = []
        for i in nums:  //先將nums中的數字依次讀入字典
            if i in dic_fre:
                dic_fre[i]+=1 //如果字典中存在同樣的i值,說明是第2次出現,將dic_fre[i]位置上的值累加計數
            else:
                dic_fre[i] = 1 //如果沒有, 則將dic_fre[i]位置上的值置1
        for i in dic_fre:
            heapq.heappush(heap_max,(-dic_fre[i],i)) //將字典中的key讀入大頂堆(push)
        for j in range(k):
            p = heapq.heappop(heap_max) // 將大頂堆中出現頻率前k的取出(pop)
            ans.append(p[1])  //保存為list形式輸出
        return ans

方法4:

定義一個字典來記錄列表nums中每個元素出現的次數,計入字典
然后將字典按值逆向排序,輸出前k個鍵.

class Solution:
    def topKFrequent(self, nums: List[int], k: int) -> List[int]:
        dic = {}
        for i in nums:
            if i in dic:
                dic[i] += 1
            else:
                dic[i] = 0
        res = sorted(dic.items(),key=lambda x:x[1], reverse=True)
        return [res[i][0] for i in range(k)]

4.滑動窗口

題目:

leetcode 209: 長度最小的子數組

給定一個含有 n 個正整數的數組和一個正整數 s ,找出該數組中滿足其和 ≥ s 的長度最小的子數組。如果不存在符合條件的子數組,返回 0。

示例:

輸入: [2,3,1,2,4,3], s = 7
輸出: 2
解釋: 子數組 [4,3] 是該條件下的長度最小的子數組。
方法1:

首先想到的方案是暴力破解,通過遍歷所有的子數組找到滿足條件的最小子數組。

class Solution:
    def minSubArrayLen(self, s, nums):
        """
        :type s: int
        :type nums: List[int]
        :rtype: int
        """
        minLen = len(nums) + 1
        for i, _ in enumerate(nums):
            sum_all = 0
            for j, tmp in enumerate(nums[i:]):
                sum_all += tmp
                if sum_all >= s:
                    minLen = min(minLen, j + 1)
        if minLen == len(nums) + 1:
            return 0

        return minLen

這種方法屬於O(n^2)解法,我們通過分析發現我們上述算法中有大量重疊運算。我們有沒有更快的做法?

這個時候我們就會想到之前Leetcode 167:兩數之和 II - 輸入有序數組提到的對撞指針。我們可以建立兩個指針,通過累加兩個指針的區間內的值和s比較,就可以在O(n)級別的時間內得到結果。

2 3 1 2 4 3 l <- -> r

由於兩個指針移動的過程中,指針之間的距離就像一個窗口一樣,我們通過控制窗口的大小,得到我們想要的結果。我們稱這種問題是一個滑動窗口問題。

class Solution:
    def minSubArrayLen(self, s, nums):
        """
        :type s: int
        :type nums: List[int]
        :rtype: int
        """
        l = 0
        r = 0
        sum_all = 0
        nums_len = len(nums)
        minLength = nums_len + 1
        while l < nums_len:
            if r < nums_len and sum_all < s://如果右指針未出界(<nums_len)且窗口內元素之和尚且小於s
                sum_all += nums[r] //將目前的sum_all加上目前的nums[r]值
                r += 1 //且右指針右移一位
            else:
                sum_all -= nums[l] //否則, 左指針需要向右滑動一位, 則原來窗口中的nums[l]被移出窗外,需減去.
                l += 1 //左指針右移一位

            if sum_all >= s: //窗口內數字之和滿足條件,在minLength和窗口長度中(r-l)中選擇最小的作為最終的minLength
                minLength = min(minLength, r - l)

        if minLength == nums_len + 1:
            return 0

        return minLength

leetcode 438: 找出所有變位詞(即元素位置/順序發生了變化,但是整體元素一致)

Given a string s and a non-empty string p, find all the start indices of p’s anagrams in s.

Strings consists of lowercase English letters only and the length of both strings s and p will not be larger than 20,100.

The order of output does not matter.

Example 1:

Input:
s: “cbaebabacd” p: “abc”

Output:
[0, 6]

Explanation:
The substring with start index = 0 is “cba”, which is an anagram of “abc”.
The substring with start index = 6 is “bac”, which is an anagram of “abc”.
Example 2:

Input:
s: “abab” p: “ab”

Output:
[0, 1, 2]

Explanation:
The substring with start index = 0 is “ab”, which is an anagram of “ab”.
The substring with start index = 1 is “ba”, which is an anagram of “ab”.
The substring with start index = 2 is “ab”, which is an anagram of “ab”.

本題是滑動窗口方法的一個應用,另外,本題處理hash的方式也是被經常使用的,需要注意。

哈希方式

分析題目,要找的是待查詢序列的一個子序列,這個子序列可以是給定目標序列的任意順序。我們首先要考慮如何滿足所謂的任意順序。這里很容易想到采取一種哈希策略,使包含目標序列的字符擁有相同的哈希值,從而任何順序的字符序列都可以被快速識別出來。這樣的想法是完全正確的,相比去產生所有可能的序列,這種方法無疑擁有更低的時間復雜度。這種方法還是比較好想到,只需要一個字符數組,保存目標序列各個字符出現的次數即可。這樣,只要滿足對應數組結構的子序列,就是一個符合要求的子序列。

🔺滑動窗口(sliding window)

字符串匹配問題一個常見的解法就是滑動窗口了,我們利用上述得到的哈希數組,來移動滑動窗口,若窗口內的字符串滿足該數組,我們就記錄窗口左部的下標作為返回值之一。本題中,滑動窗口的大小是判斷是否移動左側下標的條件,只有滑動窗口的長度達到目標序列后,左側下標才會前移一位。另外,本題中我們使用變量count來表示窗口內剩余的目標字符個數,該個數為0表示窗口內的字符是要求的字符,這個值會在窗口滑動過程中得到更新。

class Solution:
    def findAnagrams(self, s, p):
        """
        :type s: str
        :type p: str
        :rtype: List[int]
        """
        from collections import Counter
        s_len, p_len = len(s), len(p) //源string和子string的長度,本例s_len=10, p_len=3
        count = p_len //設置計數器初始大小=子string的長度
        pChar = Counter(p) //pChar: Counter({'a':1, 'b':1, 'c':1})

        result = [] //初始化一個空list,記錄結果子數組的下標起始位置的index,並且作為最后的結果返回輸出.
        for i in range(s_len)://遍歷源數組,i:0-10
            if pChar[s[i]] >= 1: //pChar[s[i]]: pChar[s[0]]=pChar[c]=1
                count -= 1 //count減1. count為0表示沒有需要匹配的字符了,此時窗口內的字符都是要求的字符.
            pChar[s[i]] -= 1 //pChar[*]字符數量減1
            if i >= p_len://當遍歷到的索引值大於子數組長度
                if pChar[s[i - p_len]] >= 0: //說明我們去掉了一個屬於p的字符,所以需要匹配的字符數count需要增加一個,count自增1
                    count += 1
                pChar[s[i - p_len]] += 1 //pChar[*]字符數量加1
            if count == 0://count為0,表示沒有需要匹配的字符了,即所有字符均被匹配到了。將有效數組起始位置index存入result中。
                result.append(i - p_len + 1) //記錄有效數組起始位置index,index計算方式:i - p_len + 1

        return result


if __name__ == "__main__":
    s = "cbaebabacd"
    p = "abc"
    print(Solution().findAnagrams(s, p))

 *如果上述程序運行不了,把注釋去掉即可。

以免理解仍有難度,再貼上一個解釋:

首先統計字符串p的字符個數,然后用兩個變量left和right表示滑動窗口的左右邊界,用變量count表示字符串p中需要匹配的字符個數,然后開始循環,如果右邊界的字符已經在哈希表中了,說明該字符在p中有出現,則count自減1,然后哈希表中該字符個數自減1,右邊界自加1,如果此時count減為0了,說明p中的字符都匹配上了,那么將此時左邊界加入結果result中。如果此時right和left的差為p的長度,說明此時應該去掉最左邊的一個字符,我們看如果該字符在哈希表中的個數大於等於0,說明該字符是p中的字符,為啥呢,因為上面我們有讓每個字符自減1,如果不是p中的字符,那么在哈希表中個數應該為0,自減1后就為-1,所以這樣就知道該字符是否屬於p,如果我們去掉了屬於p的一個字符,count自增1.

leetcode 76:最小覆蓋字串

給你一個字符串 S、一個字符串 T,請在字符串 S 里面找出:包含 T 所有字母的最小子串。

示例:

輸入: S = "ADOBECODEBANC", T = "ABC"
輸出: "BANC"
說明:

如果 S 中不存這樣的子串,則返回空字符串 ""。
如果 S 中存在這樣的子串,我們保證它是唯一的答案。

分析:

題目不難理解,就是說要在 S(source) 中找到包含 T(target) 中全部字母的一個子串,順序無所謂,但這個子串一定是所有可能子串中最短的。

滑動窗口算法的思路是這樣:

1、我們在字符串 S 中使用雙指針中的左右指針技巧,初始化 left = right = 0,把索引閉區間 [left, right] 稱為一個「窗口」。

2、我們先不斷地增加 right 指針擴大窗口 [left, right],直到窗口中的字符串符合要求(包含了 T 中的所有字符)。

3、此時,我們停止增加 right,轉而不斷增加 left 指針縮小窗口 [left, right],直到窗口中的字符串不再符合要求(不包含 T 中的所有字符了)。同時,每次增加 left,我們都要更新一輪結果。

4、重復第 2 和第 3 步,直到 right 到達字符串 S 的盡頭。

這個思路其實也不難,第 2 步相當於在尋找一個「可行解」,然后第 3 步在優化這個「可行解」,最終找到最優解。左右指針輪流前進,窗口大小增增減減,窗口不斷向右滑動。

下面畫圖理解一下,needs 和 window 相當於計數器,分別記錄 T 中字符出現次數和窗口中的相應字符的出現次數。

初始狀態:

 

 

 增加 right,直到窗口 [left, right] 包含了 T 中所有字符:

 

 

現在開始增加 left,縮小窗口 [left, right]。

 

 

 

 

直到窗口中的字符串不再符合要求,left 不再繼續移動。

 

 

 

之后重復上述過程,先移動 right,再移動 left…… 直到 right 指針到達字符串 S 的末端,算法結束。

如果你能夠理解上述過程,恭喜,你已經完全掌握了滑動窗口算法思想。至於如何具體到問題,如何得出此題的答案,都是編程問題,等會提供一套模板,理解一下就會了。

string s, t;
// 在 s 中尋找 t 的「最小覆蓋子串」
int left = 0, right = 0;
string res = s;

while(right < s.size()) {
    window.add(s[right]);
    right++;
    // 如果符合要求,移動 left 縮小窗口
    while (window 符合要求) {
        // 如果這個窗口的子串更短,則更新 res
        res = minLen(res, window);
        window.remove(s[left]);
        left++;
    }
}
return res;

如果上述代碼你也能夠理解,那么你離解題更近了一步。現在就剩下一個比較棘手的問題:如何判斷 window 即子串 s[left...right] 是否符合要求,是否包含 t 的所有字符呢?

可以用兩個哈希表當作計數器解決。用一個哈希表 needs 記錄字符串 t 中包含的字符及出現次數,用另一個哈希表 window 記錄當前「窗口」中包含的字符及出現的次數,如果 window 包含所有 needs 中的鍵,且這些鍵對應的值都大於等於 needs 中的值,那么就可以知道當前「窗口」符合要求了,可以開始移動 left 指針了。

現在將上面的框架繼續細化:

string s, t;
// 在 s 中尋找 t 的「最小覆蓋子串」
int left = 0, right = 0;
string res = s;

// 相當於兩個計數器
unordered_map<char, int> window;
unordered_map<char, int> needs;
for (char c : t) needs[c]++;

// 記錄 window 中已經有多少字符符合要求了
int match = 0; 

while (right < s.size()) {
    char c1 = s[right];
    if (needs.count(c1)) {
        window[c1]++; // 加入 window
        if (window[c1] == needs[c1])
            // 字符 c1 的出現次數符合要求了
            match++;
    }
    right++;

    // window 中的字符串已符合 needs 的要求了
    while (match == needs.size()) {
        // 更新結果 res
        res = minLen(res, window);
        char c2 = s[left];
        if (needs.count(c2)) {
            window[c2]--; // 移出 window
            if (window[c2] < needs[c2])
                // 字符 c2 出現次數不再符合要求
                match--;
        }
        left++;
    }
}
return res;

上述代碼已經具備完整的邏輯了,只有一處偽碼,即更新 res 的地方,不過這個問題太好解決了,直接看解法吧!

string minWindow(string s, string t) {
    // 記錄最短子串的開始位置和長度
    int start = 0, minLen = INT_MAX;
    int left = 0, right = 0;
    
    unordered_map<char, int> window;
    unordered_map<char, int> needs;
    for (char c : t) needs[c]++;
    
    int match = 0;
    
    while (right < s.size()) {
        char c1 = s[right];
        if (needs.count(c1)) {
            window[c1]++;
            if (window[c1] == needs[c1]) 
                match++;
        }
        right++;
        
        while (match == needs.size()) {
            if (right - left < minLen) {
                // 更新最小子串的位置和長度
                start = left;
                minLen = right - left;
            }
            char c2 = s[left];
            if (needs.count(c2)) {
                window[c2]--;
                if (window[c2] < needs[c2])
                    match--;
            }
            left++;
        }
    }
    return minLen == INT_MAX ?
                "" : s.substr(start, minLen);
}

如果直接甩給你這么一大段代碼,我想你的心態是爆炸的,但是通過之前的步步跟進,你是否能夠理解這個算法的內在邏輯呢?你是否能清晰看出該算法的結構呢?

這個算法的時間復雜度是 O(M+N),M 和 N 分別是字符串 S 和 T 的長度。因為我們先用 for 循環遍歷了字符串 T 來初始化 needs,時間 O(N),之后的兩個while 循環最多執行 2M 次,時間O(M)。

讀者也許認為嵌套的 while 循環復雜度應該是平方級,但是你這樣想,while 執行的次數就是雙指針 left 和 right 走的總路程,最多是 2M 嘛。

*這一段不是python編程,但是算法思想很通透,值得借鑒學習。附上一個python滑窗代碼:

class Solution:
    def minWindow(self, s: 'str', t: 'str') -> 'str':
        from collections import defaultdict
        lookup = defaultdict(int)
        for c in t:
            lookup[c] += 1
        start = 0
        end = 0
        min_len = float("inf")
        counter = len(t)
        res = ""
        while end < len(s):
            if lookup[s[end]] > 0:
                counter -= 1
            lookup[s[end]] -= 1
            end += 1
            while counter == 0:
                if min_len > end - start:
                    min_len = end - start
                    res = s[start:end]
                if lookup[s[start]] == 0:
                    counter += 1
                lookup[s[start]] += 1
                start += 1
        return res

附加題:

1、數組中重復的數字

在一個長度為n的數組里的所有數字都在0到n-1的范圍內。 數組中某些數字是重復的,但不知道有幾個數字是重復的。也不知道每個數字重復幾次。請找出數組中任意一個重復的數字。 例如,如果輸入長度為7的數組{2,3,1,0,2,5,3},那么對應的輸出是第一個重復的數字2。

方法1:利用字典(哈希)

# -*- coding:utf-8 -*-
class Solution:
    # 這里要特別注意~找到任意重復的一個值並賦值到duplication[0]
    # 函數返回True/False
    def duplicate(self, numbers,duplication):
        dic = {}
        for num in numbers:
            if not num in dic:
                dic[num] = 1
            else:
                dic[num] += 1
        for num in numbers:
            if dic[num] != 1:
                duplication[0] = num
                return True
        return False

方法2:高贊回答里的“高級方法”

前排說一下,這種方法並不能夠輸出“第一個重復的數字”,舉例:[3,2,1,1,3],第一個重復的數字是3,但是輸出是1。
# -*- coding:utf-8 -*-
class Solution:
    # 這里要特別注意~找到任意重復的一個值並賦值到duplication[0]
    # 函數返回True/False
    def duplicate(self, numbers, duplication):
        index = 0
        while index < len(numbers):
            if numbers[index] == index:
                index += 1
            elif numbers[index] == numbers[numbers[index]]:
                duplication[0] = numbers[index]
                return True
            else:
                index_2 = numbers[index]
                numbers[index],numbers[index_2] = numbers[index_2],numbers[index]
        return False

2.構建乘積數組

給定一個數組A[0,1,...,n-1],請構建一個數組B[0,1,...,n-1],其中B中的元素B[i]=A[0]*A[1]*...*A[i-1]*A[i+1]*...*A[n-1]。不能使用除法。(注意:規定B[0] = A[1] * A[2] * ... * A[n-1],B[n-1] = A[0] * A[1] * ... * A[n-2];)

思路一
兩重循環,在遍歷數組A的時候,A[i]賦值為1,計算B[i]。時間復雜度為O(N^2)

-*- coding:utf-8 -*-
class Solution:
    def multiply(self, A):
        # write code here
        length = len(A)
        B = []
        if(length == 0):
            return B
        for i in range(length):
            temp = A[i]
            A[i] = 1
            Bi = 1
            for j in A:
                Bi *= j
            B.append(Bi)
            A[i] = temp
        return B

or

# -*- coding:utf-8 -*-
class Solution:
    def multiply(self, A):
        # write code here
        B = []
        length = len(A)
        for i in range(length):
            count = 1
            for j in range(length):
                if j == i:
                    continue
                count *= A[j]
            B.append(count)
        return B

思路二
時間復雜度為O(N)
可以把B[i]=A[0]A[1]....A[i-1]A[i+1]....A[n-1]。看成A[0]A[1].....A[i-1]和A[i+1].....A[n-2]A[n-1]兩部分的乘積。
即通過A[i]項將B[i]分為兩部分的乘積。效果相當於是個對角矩陣。

第一個for循環用來計算上圖1范圍的數,第二個for循環用來計算上圖2范圍的數。

# -*- coding:utf-8 -*-
class Solution:
    def multiply(self, A):
        # write code here
        length = len(A)
        if(length == 0):
            return []
        import copy  
        B = copy.copy(A)
        B[0] = 1
        # 計算下三角連乘
        for i in range(1,length):
            B[i] =  B[i-1] * A[i-1]
        # 計算上三角連乘
        temp = 1
        for j in range(length-2,-1,-1):
            temp *= A[j+1]
            B[j] *= temp
        return B

or

# -*- coding:utf-8 -*-
class Solution:
    def multiply(self, A):
        # write code here
        # 計算上三角形
        length = len(A)
        B=[None]*length
        B[0]=1
        for i in range(1,length):
            B[i] = B[i-1]*A[i-1]
        temp = 1
        n = length-2
        while n>=0:
            temp *= A[n+1]
            B[n] *= temp
            n-=1
        return B

3.二維數組中的查找

在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。

方法1:順序查找

def Find(self, target, array):
    # write code here
    if len(array)==0 or len(array[0])==0:
        return 0
    l=0
    r=len(array[0])-1
    for i in range(len(array)):
        if array[i][0]<=target and targe<=array[i][-1]:
            for j in range(len(array[i])):
                if array[i][j]==target:
                    return True
    return False

方法2:二分查找

def Find(self, target, array):
    # write code here
    if len(array)==0 or len(array[0])==0:
        return 0
 
    for i in range(len(array)):
        if array[i][0]<=target<=array[i][-1]:
            l=0
            r=len(array[0])-1
            while(l<=r):
                mid=l+(r-l)//2
                if array[i][mid]<target :
                    l=mid+1
                elif array[i][mid]>target:
                    r=mid-1
                else:
                    return True
 
    return False

附:繼續分享一個思路通透的

題目描述

在一個二維數組中(每個一維數組的長度相同),每一行都按照從左到右遞增的順序排序,每一列都按照從上到下遞增的順序排序。請完成一個函數,輸入這樣的一個二維數組和一個整數,判斷數組中是否含有該整數。

1
2
3
4
1  2  8   9
2  4  9  12
4  7 10  13
6  8 11  15

思路

無意間翻看同學桌上的《劍指 Offer》,看到了這道題,我隱約覺得應該使用二分查找。考慮了一番想到了如下解決方案。

考慮上面的例子,如果我要查找 10,通過觀察最右邊的哪一列,我就知道第一行是可以排除的,因為第一行中最大的元素還小於 10。同理觀察最后一行,我可以發現前兩列是可以排除的,因為這兩列最大的元素還小於 10。因此,余下的就是下面這些了:

1
2
3
9  12
10  13
11  15

我再觀察第 1 列,發現最后一行是可以排除的,因為最后一行最小的數 11 是大於 10 的。同理,我觀察第 1 行,發現最后一列是可以排除的,因為這一列中最小的元素 12 大於 10。這樣以來就只余下這些了:

1
2
9 
10

基於以上發現,可以給行和列維護一個范圍。交替地在余下的矩陣的左上方和右下方做二分查找,來縮小范圍。下面是我的實現:

import numpy as np
 
def upper_bound(a, x, lo=0, hi=None):
    """
    find the first element great than x in a sorted list
    return the index of this element
    """
    if hi is None:
        hi = len(a)
 
    while lo < hi:
        mid = (lo + hi) // 2
        if a[mid] <= x:
            lo = mid + 1
        else:
            hi = mid
    return lo
 
def lower_bound(a, x, lo=0, hi=None):
    """
    find the first element that is not less than x (i.e. greater or equal to)
    """
    if hi is None:
        hi = len(a)
 
    while lo < hi:
        mid = (lo+hi)//2
        if a[mid] < x:
            lo = mid + 1
        else:
            hi = mid
    return lo
 
class Solution:
    def Find(self, matrix, x):
        if len(matrix) == 0 or len(matrix[0]) == 0:
            return False
        matrix = np.array(matrix)
        row_lo, row_hi = 0, matrix.shape[0]
        col_lo, col_hi = 0, matrix.shape[1]
 
        while True:
            row_lo = lower_bound(matrix[:, col_hi-1], x, row_lo, row_hi)
            col_lo = lower_bound(matrix[row_hi-1], x, col_lo, col_hi)
 
            if row_lo == row_hi or col_lo == col_hi:
                break
 
            row_hi = upper_bound(matrix[:, col_lo], x, row_lo, row_hi)
            col_hi = upper_bound(matrix[row_lo], x, col_lo, col_hi)
 
            if row_lo == row_hi or col_lo == col_hi:
                break
 
            if matrix[row_lo, col_lo] == x or matrix[row_lo, col_hi-1] == x:
                return True
            if matrix[row_hi-1, col_lo] == x or matrix[row_hi-1, col_hi-1] == x:
                return True
 
        return False

or

# -*- coding:utf-8 -*-
class Solution:
    # array 二維列表
    def Find(self, target, array):
        # write code here
        #方法二  從右上角開始進行搜索
        if not array:
            return False
        rows = len(array)
        columns = len(array[0])
        row = 0
        column = columns - 1
        while row < rows and column >= 0:
            if array[row][column] == target:
                return True
            if array[row][column] > target:
                column -= 1
            else:
                row += 1
        return False

or 暴力搜索:

#暴力搜索
for arr in array:
   if target in arr:
     return True
   return False         


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM