高頻算法題之數組詳細分析


大家好,我是程序員學長~

今天給大家帶來一篇面試高頻算法題之數組的詳細解析,全文包含19道大廠筆試面試算法真題,一舉拿下數組這個知識點,讓算法不在成為進入大廠的絆腳石。

如果喜歡,記得點個關注喲~

本文有點長,我已將本文制作成帶目錄的PDF版本,獲取本文PDF版本,請私信我。

全文概覽

數組的基礎知識

數組的定義及特點

數組是一種線性表數據結構,是在連續內存空間上的存儲相同類型數據的集合。

數組主要有以下特點。

  1. 數組的下標是從0開始的。
  2. 連續的內存空間和相同的數據類型。

正是因為數組的內存空間是連續的,所以我們可以“隨機訪問”數組內的元素。但有利有弊,如果想要在數組中插入或者刪除一個元素,為了保證數組的內存空間的連續,就難免要移動其他元素。

例如要刪除下標為2的元素,需要對下標2后面的元素都需要向前移動。如圖所示:

解題有妙招

二分法

如果給定的數組是有序的,我們就需要考慮是否可以使用二分法來求解(二分法的時間復雜度是O(logn))。面試中二分法是面試中常考的知識點,建議大家一定要多鍛煉手撕二分的能力。

雙指針法

我們可以通過一個快指針和慢指針在一個for循環下完成兩個for循環的工作。例如,當我們需要枚舉數組中的兩個元素時,如果我們發現隨着第一個元素的遞增,第二個元素是遞減的,那么就可以使用雙指針的方法,將枚舉的時間復雜度從O(N^2)減低到O(N)。

滑動窗口

顧名思義,所謂的滑動窗口就是可以在一個序列上進行滑動的窗口。其中窗口大小有固定長度的,也有可變長度的。例如給定數組[2,2,3,4,8,99,3],窗口大小為3,求出每個窗口的元素和就是固定大小窗口的問題,如果求數組[2,2,3,4,8,99,3]的最長連續子數組就是窗口可變的問題。使用滑動窗口,我們可以減低算法是時間復雜度。

使用滑動窗口求解問題時,主要需要了解什么條件下移動窗口的起始位置,以及何時動態的擴展窗口,從而解決問題。

哈希表法

如果需要在數組中查找某個元素是否存在時,我們可以使用哈希表法,可以將查找元素的時間復雜度從O(n)減低到O(1)。

兩數之和

問題描述

LeetCode 1. 兩數之和

給定一個整數數組nums和一個整數目標值target,請你在該數組中找出和為目標值target的那兩個整數,並返回它們的數組下標。你可以假設每種輸入只會對應一個答案。但是,數組中同一個元素在答案里不能重復出現。你可以按任意順序返回答案。

示例 1:

輸入:nums = [2,7,11,15], target = 9

輸出:[0,1]

解釋:因為 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

分析問題

拿到這個問題,最簡單直觀的想法就是對於數組中的每個元素x,去尋找數組中是否存在target-x。

def twoSum(nums, target):
    n = len(nums)
    for i in range(n):
        #對於數組中的每個元素i
        #位於它之前的元素都已經和它匹配過了,不需要重復進行匹配
        for j in range(i + 1, n):
            if nums[i] + nums[j] == target:
                return [i, j]
    return []

我們可以很清楚的知道,這個算法的時間復雜度是O(n^2)。那我們該如何降低時間復雜度呢?可以注意到該算法復雜度高的原因在於尋找元素target-x的時候,需要遍歷一遍數組,所以我們需要對這一塊進行優化。我們可以采用哈希表,將尋找元素target-x的時間復雜度由O(n)降低到O(1)。

我們在遍歷數組時,對於每個元素x,我們首先查詢哈希表中是否存在target-x,如果存在,將匹配到的結果直接返回,如果不存在,我們把x插入到哈希表中。

Tips: 我們這里使用字典來代替哈希表,當插入的元素重復時,我們覆蓋就好了,這樣可以保證查找的時間復雜度為O(1)。為什么這里可以覆蓋呢,因為題目要求是找兩個數和等於target,當有兩個元素重復時,我們認為它們是等價的,所以我們只需要保留一個就好了。

def twoSum(nums, target):
    hash_table = dict()
    for i, num in enumerate(nums):
        if hash_table.__contains__(target-num):
            return [hash_table[target - num], i]
        hash_table[nums[i]] = i
    return []

nums = [2,7,11,15]
target = 9
print(twoSum(nums,target))

我們可以看到該算法的時間復雜度是O(n),空間復雜度也是O(n)。

優化

當我們需要枚舉數組中的兩個元素時,如果我們發現隨着第一個元素的遞增,第二個元素是遞減的,那么就可以使用雙指針的方法,將枚舉的時間復雜度從O(N^2)減低到O(N)。 所以我們可以采用雙指針法來求解。首先,我們先對數組進行排序,然后用left和right指針分別指向數組的左邊和右邊,此時sum=nums[left]+nums[right],根據sum和target的大小關系,我們來移動指針。

  1. 如果sum>target,右指針左移,減小sum的值,即right=right-1。
  2. 如果sum<target,左指針右移,增大sum的值,即left=left+1。
  3. 如果sum=target,直接返回。

下面我們來看一下代碼的實現。

def twoSum(nums, target):
    nums=sorted(nums)
    left=0
    right=len(nums)-1
    while left < right:
        sum=nums[left]+nums[right]
        if sum>target:
            right=right-1
        elif sum<target:
            left=left+1
        else:
            return [left,right]

利用sorted進行排序,時間復雜度是O(nlogn),空間復雜度是O(n)。所以該算法的時間復雜度是O(nlogn),空間復雜度是O(n)。

最長無重復子數組

問題描述

LeetCode3. 無重復字符的最長子串

給定一個數組arr,返回arr的最長無重復元素子數組的長度,無重復指的是所有數字都不相同。子數組是連續的,比如[1,3,5,7,9]的子數組有[1,3],[3,5,7]等等,但是[1,3,7]不是子數組。

示例

輸入:[2,2,3,4,8,99,3]

返回值:5

說明:[2,3,4,8,99]是最長子數組

分析問題

在開始之前,我們首先介紹一下什么是滑動窗口。顧名思義,所謂的滑動窗口就是可以在一個序列上進行滑動的窗口。如圖所示,假設,我們的序列為abcabcbb,我們這里定義了一個固定大小為3的窗口在序列上滑來滑去。

在實際的使用中,我們使用的滑動窗口都是可變長度的。

我們可以使用雙指針來維護窗口的開始和結束,通過移動左、右指針來實現窗口大小的改變和窗口的滑動。

我們來看一下題目,題目是求最長無重復元素子數組,如果我們可以求出所有的無重復元素的子數組,那取出最長的不就好了。下面我們來看一下如何求解。我們只需要維護一個在數組中進行滑動的窗口就好。

  1. 開始時2不在窗口中,所以擴大窗口。

  1. 下一個元素2在窗口中出現,所以我們要將出現過的元素及其左邊的元素統統移出窗口,即2。

  1. 接下來的元素3、4、8、99都沒在窗口中出現過,所以我們把它們都加入到窗口中。

  1. 下一個元素3在窗口出現過,所以我們要移除出現過的元素及其左邊的元素,即2,3。

下面我們來看一下代碼如何實現。

    if not s:
        return 0
    left = 0
    # 記錄每個字符是否出現過
    window = set()
    n = len(s)
    max_len = 0
    for i in range(n):
        #如果出現過,移除重復元素及其左邊的元素
        while s[i] in window:
            window.remove(s[left])
            left += 1
        #沒出現過,加入window
        window.add(s[i])

        max_len = max(len(window),max_len)
        
    return max_len

該算法的時間復雜度是O(n)。

合並兩個有序數組

問題描述

LeetCode 88. 合並兩個有序數組

給你兩個按非遞減順序排列的整數數組nums1和nums2,另有兩個整數m和n,分別表示nums1和nums2中的元素數目。請你合並nums2到nums1中,使合並后的數組同樣按非遞減順序排列。

注意:最終,合並后數組不應由函數返回,而是存儲在數組nums1中。為了應對這種情況,nums1的初始長度為m + n,其中前 m 個元素表示應合並的元素,后 n 個元素為 0 ,應忽略。nums2 的長度為 n 。

示例:

輸入:nums1 = [1,2,3,0,0,0], m = 3,nums2 = [2,5,6],n = 3

輸出:[1,2,2,3,5,6]

解釋:需要合並 [1,2,3] 和 [2,5,6] 。合並結果是 [1,2,2,3,5,6] 。

分析問題

最簡單暴力的方法就是直接把nums2放入nums1的后n個位置,然后直接對nums1進行排序就好了。我們這里就不在贅述。

def merge(nums1, m, nums2, n):
    nums1[m:] = nums2
    nums1.sort()

nums1 = [1,2,3,0,0,0]
m = 3
nums2 = [2,5,6]
n = 3
merge(nums1,m,nums2,n)
print(nums1)

那么既然給定的兩個數組是有序的,那我們何不把這個條件利用起來,來優化代碼。所以,我們可以使用兩個指針p1和p2分別指向兩個數組的起始位置,然后比較大小,將較小的放入結果中,然后指針后移,直到將所有元素都排好序。

下面我們來看一下代碼的實現。

def merge(nums1, m, nums2, n):
    #暫時存放排好序的元素
    sorted = []
    p1, p2 = 0, 0
    #沒有遍歷完數組時
    while p1 < m and p2 < n:
        #p1元素遍歷完
        if nums1[p1] <= nums2[p2]:
            sorted.append(nums1[p1])
            p1 += 1
        else:
            sorted.append(nums2[p2])
            p2 += 1
    if p1 == m:
        for x in nums2[p2:]:
            sorted.append(x)
    else:
        for x in nums1[p1:m]:
            sorted.append(x)
    nums1[:] = sorted

nums1 = [1,2,3,0,0,0]
m = 3
nums2 = [2,5,6]
n = 3
merge(nums1,m,nums2,n)
print(nums1)

我們可以知道這里的時間復雜度是O(m+n),空間復雜度也是O(m+n)。

優化

在使用雙指針法時,我們從前往后遍歷數組,如果直接使用nums1存儲合並結果的話,nums1 中的元素可能會在取出之前被覆蓋。所以我們引入了一個臨時變量sorted來存儲。那有什么辦法可以避免nums1中的元素被覆蓋呢?既然從前往后不可以,那么我們能從后往前嗎?因為nums1的后半部分是空的,可以直接覆蓋而不會影響結果,所以這里引入了逆向雙指針法。

我們來看一下代碼實現。

def merge(nums1, m, nums2, n):
    #指向數組的末尾元素
    p1 = m -1
    p2 = n - 1
    tail = m + n - 1
    while p1 >= 0 or p2 >= 0:
        #表示nums1遍歷完成
        if p1 == -1:
            nums1[tail] = nums2[p2]
            p2 -= 1
        #表示nums2遍歷完成
        elif p2 == -1:
            nums1[tail] = nums1[p1]
            p1 -= 1
        #將大的元素合並
        elif nums1[p1] >= nums2[p2]:
            nums1[tail] = nums1[p1]
            p1 -= 1
        else:
            nums1[tail] = nums2[p2]
            p2 -= 1
        tail -= 1

該算法的時間復雜度是O(m+n),空間復雜度是O(1)。

螺旋矩陣

問題描述

LeetCode 54. 螺旋矩陣

給你一個 mn 列的矩陣 matrix ,請按照 順時針螺旋順序 ,返回矩陣中的所有元素。

示例:

輸入:matrix = [[1,2,3],[4,5,6],[7,8,9]]

輸出:[1,2,3,6,9,8,7,4,5]

分析問題

題目要求是按照順時針螺旋順序輸出,即先從左到右、然后從上到下、再從右到左、最后從下到上,依次類推,直到全部元素遍歷完為止。在遍歷的過程中,最關鍵的一點就是要記錄哪些元素已經被訪問過了,如果遇到被訪問過的元素,我們就需要順時針的調整一下方向。

判斷元素是否被訪問過,最直觀的想法就是聲明一個和矩陣大小相同的矩陣,來標識矩陣中的元素是否被訪問過。

我們來看一下代碼如何實現。

def spiralOrder(matrix):
    #矩陣為空,直接返回
    if not matrix or not matrix[0]:
        return []
    rows=len(matrix)
    columns=len(matrix[0])
    visited = [[False for _ in range(columns)] for _ in range(rows)]
    #總元素的個數
    count=rows*columns
    result=[0]*count
    #代表方向,即從左到右、從上到下、從右到左、從下到上
    directions = [[0, 1], [1, 0], [0, -1], [-1, 0]]
    row, column = 0, 0
    #從左上角開始遍歷
    directionIndex = 0
    for i in range(count):
        result[i] = matrix[row][column]
        #將訪問過的元素進行標記
        visited[row][column] = True
        nextRow, nextColumn = row + directions[directionIndex][0], column + directions[directionIndex][1]
        #不越界,並且已經被訪問過了,順時針調整方向
        if not (0 <= nextRow < rows and 0 <= nextColumn < columns and not visited[nextRow][nextColumn]):
            directionIndex = (directionIndex + 1) % 4

        row += directions[directionIndex][0]
        column += directions[directionIndex][1]
    return result

由於創建了一個和原始矩陣一樣大小的矩陣來表示元素是否被訪問過,所以該算法的空間復雜度是O(mn)。矩陣中的元素都會被遍歷一次,所以該算法的時間復雜度也是O(mn)。

優化

那有什么方法可以優化算法的空間復雜度嗎?即我們不用開辟新的空間來保存矩陣中的元素是否被訪問過。

其實我們可以在遍歷的過程中不斷的改變邊界條件,當矩陣的第一行元素被訪問過之后,那上邊界就需要進行+1操作;如果最后一列元素被訪問過了,那么右邊界需要進行-1操作;如果最后一行元素被訪問過了,那下邊界需要進行-1操作;如果第一列被訪問過了,那需要進行+1操作;依次類推,直到遍歷完成。

def spiralOrder(matrix):
    # 矩陣為空,直接返回
    if not matrix or not matrix[0]:
        return []
    rows = len(matrix)
    columns = len(matrix[0])
    result=[]
    #開始時,左、右、上、下邊界
    left=0
    right=columns-1
    up=0
    down=rows-1
    while True:
        #從左到右
        for i in range(left,right+1):
            result.append(matrix[up][i])
        #上邊界調整
        up=up+1
        #越界,退出
        if up>down:
            break
        #從上到下
        for i in range(up,down+1):
            result.append(matrix[i][right])
        #右邊界調整
        right=right-1
        # 越界,退出
        if right<left:
            break
        #從右到左
        for i in range(right,left-1,-1):
            result.append(matrix[down][i])
        #下邊界調整
        down=down-1
        if down<up:
            break
        #從下到上
        for i in range(down,up-1,-1):
            result.append(matrix[i][left])
        left=left+1
        if left>right:
            break
    return result

該算法的時間復雜度是O(m*n),空間復雜度是O(1)。

數組中和為 0 的三個數

問題描述

LeetCode 劍指 Offer II 007. 數組中和為 0 的三個數

給定一個包含n個整數的數組nums,判斷nums中是否存在三個元素 a ,b ,c ,使得 a + b + c = 0 ?請找出所有和為 0 且不重復的三元組。

示例:

輸入:nums = [-1,0,1,2,-1,-4]

輸出:[[-1,-1,2],[-1,0,1]]

分析問題

這道題目算是二數之和的升級版,所以我們也可以采用雙指針法來求解。那三個數如何采用雙指針法呢,其實很簡單,我們先把一個數固定下來,然后另外兩個數再使用雙指針去尋找不就好了。按照慣例,我們首先對數組進行排序,然后固定第一個數first,假設為nums[i],然后再使用雙指針法在數組中尋找兩數之和等於-nums[i]即可。由於題目要求所求的三元組是不重復的,所以需要判斷去掉重復解。重復解主要有以下兩種情況。

  1. nums[first]=nums[first-1],由於我們已經把第一個元素是nums[first-1]的三元組已經求解過了,所以沒必要重復求解。
  2. nums[first]+nums[left]+nums[right]=0時,如果nums[left]=nums[left+1]或者nums[right]=nums[right+1],會導致重復解,所以需要去掉。

我們來看一下代碼的實現。

def threeSum(nums):
    n=len(nums)
    result=[]
    if n<3:
        return result
    nums=sorted(nums)
    print(nums)
    #遍歷數組
    for i in range(n):
        #固定第一個數
        first=nums[i]
        #第一個數大於0,由於第二個、第三個數都大於第一個數
        #所以不可能相加等於0
        if first>0:
            break
        #已經查找過了,所以不需要再繼續尋找,直接跳過
        if i>0 and first==nums[i-1]:
            continue
        #第三個數,開始時指向最數組的最右端
        target=-first
        right=n-1
        left=i+1
        while left<right:
            if nums[left]+nums[right]==target:
                result.append([nums[i],nums[left],nums[right]])
                #如果left和left+1對於的元素相同,由於left已經添加到result中了
                #為了避免重復,我們跳過相同的元素
                while left<right and nums[left]==nums[left+1]:
                        left=left+1
                #同理,跳過和right相同的元素
                while left<right and nums[right]==nums[right-1]:
                        right=right-1
                left=left+1
                right=right-1

            elif nums[left]+nums[right]>target:
                right=right-1
            else:
                left=left+1
    return result

nums=[-1,0,1,2,-1,-4]
print(threeSum(nums))

數組中出現次數超過一半的數字

問題描述

LeetCode 劍指 Offer 39. 數組中出現次數超過一半的數字

給一個長度為 n 的數組,數組中有一個數字出現的次數超過數組長度的一半,請找出這個數字。例如輸入一個長度為9的數組[1,2,3,2,2,2,5,4,2]。由於數字2在數組中出現了5次,超過數組長度的一半,因此輸出2。

示例:

輸入:[1,2,3,2,2,2,5,4,2]

輸出:2

分析問題

哈希表法

我們最容易想到的方法就是使用哈希表法,去統計每個數字出現的次數,即可很容易的求出眾數。

def majorityElement(nums):
    #用字典來保存每個數字出現的次數
    data_count = {}
    for x in nums:
        if x in data_count:
            data_count[x] = data_count[x] + 1
        else:
            data_count[x] = 1
    max_value=0
    max_key=0
    #遍歷字典,取出次數最大的
    #因為題目說給定的數組一定存在眾數
    #所以最大的次數就是眾數
    for key in data_count:
        value=data_count[key]
        if value>max_value:
            max_value=value
            max_key=key
    return max_key

data=[1,2,3,2,2,2,5,4,2]
print(majorityElement(data))

該算法的時間復雜度是O(n),空間復雜度也是O(n)。

排序算法

我們將數組進行排序,那排序后的數組的中點一定就是眾數。

def majorityElement(nums):
        #將數組排序
        nums.sort()
        #返回排序數組中的中點
        return nums[len(nums) // 2]

data=[1,2,3,2,2,2,5,4,2]
print(majorityElement(data))

Boyer-Moore 投票算法

這道題最經典的解法是Boyer-Moore 投票算法。Boyer-Moore 投票算法的核心思想是票數正負抵消,即遇到眾數時,我們把票數+1,遇到非眾數時,我們把票數-1,則所有的票數和一定是大於0的。

我們假設數組nums的眾數是x,數組的長度為n。我們可以很容易的知道,若數組的前a個數字的票數和為0,那么剩余n-a個數字的票數和一定是大於0的,即后n-a個數字的眾數仍然為x。

我們記數組的首個元素是n1,數組的眾數是x,遍歷並統計票數。當發生票數和為0時,數組剩余元素的眾數一定也是x,這是因為:

  1. 當n1等於x時,抵消的所有數字中,有一半是眾數x。
  2. 當n1不等於x時,抵消的所有數字中,眾數的個人最少為0,最多為一半。

所以,我們在去掉m個字符里,最多只去掉了一半的眾數,所以在剩余的n-m個元素中,x仍然為眾數。利用這個特性,在每次遇到票數和為0時,我們都可以縮小剩余數組區間。當遍歷完成時,最后一輪假設的數字即為眾數。

class Solution:
    def majorityElement(self, nums):
        #票數和
        counts=0
        for num in nums:
            #如果票數和為0,我們假設num元素為眾數
            if counts == 0:
                x = num
            #如果是眾數票數+1,否則票數-1
            if num==x:
                counts=counts+1
            else:
                counts=counts-1
        return x

該算法的時間復雜度是O(n),空間復雜度是O(1)。

合並區間

問題描述

LeetCode 56. 合並區間

以數組 intervals 表示若干個區間的集合,其中單個區間為 intervals[i] = [starti, endi] 。請你合並所有重疊的區間,並返回一個不重疊的區間數組,該數組需恰好覆蓋輸入中的所有區間。

輸入:intervals = [ [1,3],[2,6],[8,10],[15,18] ]
輸出:[ [1,6],[8,10],[15,18] ]
解釋:區間 [1,3] 和 [2,6] 重疊,將它們合並為 [1,6]

分析問題

對於任意兩個區間A和B,它們之間的關系可以有以下6種情況。

我們將這兩個區間進行比較、交換,使得第一個區間的起始位置 ≤ 第二個區間的起始位置這個條件成立,這樣的話,我們就可以把這6種情況轉換成以下3種。

按照這個思路,我們將所有區間按照左端點進行排序,那么就可以保證任意連續的兩個區間,第一個區間的起始位置 ≤ 第二個區間的起始位置,所以他們的關系只有上面三種情況。

算法

對於上面的三種情況,我們可以采用如下算法來求解。

首先,我們用數組 merged 存儲最終的答案。然后我們將第一個區間加入 merged 數組中,並按順序依次考慮之后的每個區間:

  • 如果當前區間的左端點在數組 merged 中最后一個區間的右端點之后,即上圖中的第二種情況,那么它們不會重合。我們可以直接將這個區間加入數組 merged 的末尾;

  • 否則,它們是有重合部分的,即上圖中的第一、三種情況,我們需要用當前區間的右端點更新數組 merged 中最后一個區間的右端點,將其置為二者的較大值。

這樣,我們就可以解決上述的三種情況,下面我們來看一下代碼的實現。

class Solution:
    def merge(self, intervals):
        #將區間數組按照左端點進行升序排序
        intervals.sort(key=lambda x: x[0])
        #存放合並后的結果
        merged = []
        for interval in intervals:
            #如果列表為空
            #或者當前區間的左端點大於merged最后一個元素的右端點,直接添加
            if not merged or merged[-1][1] < interval[0]:
                merged.append(interval)
            else:
                #否則的話,我們就可以與上一區間進行合並
                #修改merged最后一個元素的右端點為兩者的最大值
                merged[-1][1] = max(merged[-1][1], interval[1])
        return merged

該算法的時間復雜度是O(nlogn),其中n是區間的數量,除去排序的開銷,我們只需要一次線性掃描,所以主要的時間開銷是排序的 O(n logn)。空間復雜度是O(logn)。

在兩個長度相等的排序數組中找到上中位數

問題描述

LeetCode 4. 尋找兩個正序數組的中位數

給定兩個遞增數組arr1和arr2,已知兩個數組的長度都為N,求兩個數組中所有數的上中位數。上中位數:假設遞增序列長度為n,為第n/2個數。

要求:時間復雜度 O (n),空間復雜度 O(1)。

進階:時間復雜度為O(logN),空間復雜度為O(1)。

示例:

輸入:[1,2,3,4],[3,4,5,6]

返回值:3

說明:總共有8個數,上中位數是第4小的數,所以返回3。

分析問題

這道題最直觀的想法就是,使用歸並排序的方式,將兩個有序數組合並成一個大的有序數組。大的有序數組的上中位數為第n/2個數。我們可以知道該算法的時間復雜度是O(N),空間復雜度也是O(N),顯然不符合題目要求的O(1)的空間復雜度。其實,我們也不需要合並兩個有序數組,我們只需要找到上中位數的位置即可。對於給定兩個長度為N的數組,我們可以知道其中位數的位置為N,所以我們維護兩個指針,初始時分別指向兩個數組的下標0的位置,每次將指向較小值的指針后移一位(如果一個指針已經到達數組的末尾,則只需要移動另一個數組的指針),直到到達中位數的位置。

下面我們來看一下代碼如何實現。

class Solution:
    def findMedianinTwoSortedAray(self , arr1 , arr2 ):
        # write code here
        #數組的長度
        N=len(arr1)
        #定義兩個指針,指針兩個數組的開始位置
        p1=p2=0
        ans=0
        while p1+p2<N:
            #移動較小元素的指針位置
            if arr1[p1]<=arr2[p2]:
                ans=arr1[p1]
                p1=p1+1
            else:
                ans=arr2[p2]
                p2=p2+1

        return ans

該算法的時間復雜度是O(n),空間復雜度是O(1)。

進階

下面我們來看一下如何把時間復雜度降低為O(logN)。我們這里可以采用二分查找的思想來求解。

對於長度為N的數組arr1和arr2來說,它的上中位數是兩個有序數組的第N個元素。所以,我們把這道題目可以轉換成尋找兩個有序數組的第k小的元素,其中k=N。

要找到第N個元素,我們可以比較arr1[N/2-1]和arr2[N/2-1],其中“/”代表整數除法。由於arr1[N/2-1]和arr2[N/2-1]的前面分別有arr1[0...N/2-2]和arr2[0...N/2-2],即N/2-1個元素。對於arr1[N/2-1]和arr2[N/2-1]的較小值,最多只會有N/2-1+N/2-1=N-2個元素比它小,所以它不是第N小的元素。

因此,我們可以歸納出以下三種情況。

  • 如果arr1[N/2-1] < arr2[N/2-1],則比arr1[N/2-1] 小的數最多只有 arr1的前N/2-1個數和arr2的前N/2-1個數,即比arr1[N/2-1] 小的數最多只有N-2個,因此arr1[N/2-1]不可能是第N個數,arr1[0]到arr1[N/2-1]也都不可能是第N個數,所以可以刪除。
  • 如果arr1[N/2-1] > arr2[N/2-1],則可以排除arr2[0]到arr2[N/2-1]。
  • 如果arr1[N/2-1]==arr2[N/2-1],可以歸為第一種情況進行處理。

可以看到,經過一輪比較后,我們可以排查N/2個不可能是第N小的數,查找范圍縮小了一半。同時,我們將在排除后的新數組上繼續進行二分查找,並且根據我們排除數的個數,減少 N 的值,這是因為我們排除的數都不大於第 N 小的數。

下面我們來看一個具體的例子。

class Solution:
    def findMedianSortedArrays(self, arr1, arr2):
        N = len(arr1)
        #如果N=1,直接返回兩個數組的首元素的最小值即可
        if N==1:
            return min(arr1[0],arr2[0])

        index1, index2 = 0, 0
        #中位數位置為N,不過超過分區數組的下標
        k=N
        while k>1:
        
            new_index1 = index1 +  k // 2 - 1
            new_index2 = index2 +  k // 2 - 1
            data1, data2 = arr1[new_index1], arr2[new_index2]
            #選擇較小值,同時將前k//2個元素刪除
            if data1 <= data2:
                k=k-k//2
                #刪除前k//2個元素
                index1 = new_index1+1
            else:

                k=k-k//2
                #刪除前k//2個元素
                index2 = new_index2 + 1

        return min(arr1[index1],arr2[index2])

加餐

如果給定的兩個有序數組大小不同,即給定兩個大小分別為 mn 的正序(從小到大)數組 nums1nums2。請你找出並返回這兩個正序數組的 中位數

根據中位數的定義,當m+n為奇數時,中位數是兩個有序數組的第(m+n+1)/2個元素。當m+n為偶數時,中位數是兩個有序數組的第(m+n)/2和(m+n)/2+1個元素的平均值。因此這道題我們可以轉化成尋求兩個有序數組的第k小的數,其中k為(m+n)/2或者(m+n)/2+1。

所以該題的解題思路和上一題的解法類似,不過這里有一些情況需要特殊處理。

  • 如果nums1[k/2-1]或者nums[k/2-1]越界,那么我們需要選擇對應數組的最后一個元素,即min(k/2-1,m-1)或者min(k/2-1,n-1)。
  • 如果一個數組為空,我們可以直接返回另一個數組中第 k 小的元素。
  • 如果k=1,我們只需要返回兩個數組首元素的最小值即可。

下面我們來看一下代碼的實現。

下面我們來看一下代碼的實現。

class Solution:
    def findMedianSortedArrays(self, nums1, nums2):
        #獲取第k小的元素
        def getKthElement(k):
            #表示兩個有序數組的下標位置
            index1, index2 = 0, 0
            while True:
                #如果一個數組遍歷完成,則直接返回另一個數組的第k小元素
                if index1 == m:
                    return nums2[index2 + k - 1]
                if index2 == n:
                    return nums1[index1 + k - 1]
                #如果k為1,返回兩個有序數組首元素的最小值
                if k == 1:
                    return min(nums1[index1], nums2[index2])

                #防止數組越界,所以取index1 + k // 2 - 1和m - 1的最小值
                new_index1 = min(index1 + k // 2 - 1, m - 1)
                #同理,取index2 + k // 2 - 1和 n - 1的最小值
                new_index2 = min(index2 + k // 2 - 1, n - 1)
                data1, data2 = nums1[new_index1], nums2[new_index2]
                #如果data1<data2
                if data1 <= data2:
                    #使k的值減少new_index1 - index1 + 1多個元素
                    k -= new_index1 - index1 + 1
                    #移除元素
                    index1 = new_index1 + 1
                else:
                    #使k的值減少new_index2 - index2 + 1多個元素
                    k -= new_index2 - index2 + 1
                    #移除元素
                    index2 = new_index2 + 1

        m, n = len(nums1), len(nums2)
        #兩個數組的長度
        lens = m + n
        #如果為奇數,則中位數是第(lens+1)/2
        #如果為偶數,則中位數是lens/2和lens/2+1的平均值
        if lens % 2 == 1:
            return getKthElement((lens + 1) // 2)
        else:
            return (getKthElement(lens // 2) + getKthElement(lens // 2 + 1)) / 2.0

該算法的時間復雜度是O(log(m+n)),空間復雜度是O(1)。

缺失的第一個正整數

問題描述

LeetCode 41. 缺失的第一個正數

給你一個無重復元素未排序的整數數組 nums ,請你找出其中沒有出現的最小的正整數。

請你實現時間復雜度為O(n)並且只使用常數級別額外空間的解決方案。

示例:

輸入:nums = [1,2,0]

輸出:3

分析問題

對於一個無重復元素、長度為N的數組,其中沒有出現的最小整數只能在[1,N+1]中,這是因為如果[1,N]在數組中都出現了,說明這N個數已經把數組填滿了,那么答案是N+1,否則就是[1,N]中沒有出現的最小整數。所以,我們可以申請一個輔助數組temp,大小為N,我們通過遍歷原數組,將屬於[1,N]范圍內的數,放入輔助數組中相應的位置,使得temp[i-1] = i 成立。在遍歷完成后,temp中第一個不滿足temp[i-1] = i 條件的就是最小的正整數,如果都滿足,那么最小正整數就是N+1。

下面我們來看一下代碼實現。

class Solution:
    def firstMissingPositive(self, nums):
        n = len(nums)
        #申請一個臨時數組,存放數組中的元素
        temp = [0]*n
        for i in range(n):
            #如果整數不在[1,N]的范圍內,不做處理
            if nums[i] <= 0 or nums[i] > n:
                continue
            else:
                #否則把整數放入temp的相應位置
                temp[nums[i]-1]=nums[i]

        #遍歷temp,找到第一個不滿足temp[i]!=i+1的整數
        #就是代表數組中不存在的最小整數
        for i in range(n):
            if temp[i]!=i+1:
                return i+1
        #如果都存在,返回N+1
        return n+1

我們可以知道該算法的時間復雜度和空間復雜度都是O(n),顯然空間復雜度不滿足題目要求,那我們該如何降低算法的空間復雜度呢?通過觀察,我們可以發現輔助數組和原數組大小一樣,那么我們能否復用原數組nums呢?答案顯然是可以的。我們在遍歷數組的過程中,假設遍歷到的元素值為x,如果x屬於[1,N],我們將元素x和nums[x-1]的元素進行互換,使得x出現在正確的位置上,否則不做處理。當遍歷完成后,nums中第一個不滿足nums[i-1] = i 條件的就是最小的正整數,如果都滿足,那么最小正整數就是N+1。

下面我們來看一下代碼的實現。

class Solution:
    def firstMissingPositive(self, nums):
        #數組的長度
        n = len(nums)
        #遍歷數組,將元素放到正確的位置
        for i in range(n):
            #如果nums[i]在[1,n]的范圍內,並且nums[i]不在正確的位置上,我們進行互換
            #否則不做處理
            while 1 <= nums[i] <= n \
                    and nums[nums[i] - 1] != nums[i]:
                nums[nums[i] - 1], nums[i] = nums[i], nums[nums[i] - 1]
                
        #找到數組中第一個不滿足nums[i] != i + 1條件的
        #就是數組中的最小正整數
        for i in range(n):
            if nums[i] != i + 1:
                return i + 1
        #如果都滿足,最小的整數就是n+1    
        return n + 1

該算法的時間復雜度是O(n),空間復雜度是O(1)。

順時針旋轉數組

問題描述

LeetCode 面試題 01.07. 旋轉矩陣

有一個 NxN 整數矩陣,請編寫一個算法,將矩陣順時針旋轉90度。

要求:時間復雜度O(N2),空間復雜度是O(N2)。

進階:時間復雜度是O(N^2),空間復雜度是O(1)

示例:

[                                                       [     
  [ 5, 1, 9,11],                旋轉90度后                  [15,13, 2, 5],
  [ 2, 4, 8,10],              ============>                [14, 3, 4, 1],
  [13, 3, 6, 7],                                           [12, 6, 8, 9], 
  [15,14,12,16]                                            [16, 7,10,11]
]                                                        ]

分析問題

對於矩陣中的第一行元素來說,在經過90度旋轉后,出現在了倒數第一列的位置上,如下圖所示。

並且,第一行的第 i 個元素在旋轉后恰好是倒數第一列的第 i 個元素。對於第二行的元素也是如此,在旋轉后變成倒數第二列的元素,並且第二行的第i個元素在旋轉后恰好是倒數第二列的第i個元素。所以,我們可以得出規律,對於矩陣中第 i 行的第 j 個元素,在旋轉后,它出現在倒數第 i 列的第 j 個位置,即對於矩陣中的 matrix[i] [j] 元素,在旋轉后,它的新位置為 matrix [j] [n-i-1]。

所以,我們申請一個大小為 n * n 的新矩陣,來臨時存儲旋轉后的結果。我們通過遍歷matrix中的所有元素,根據上述規則將元素存放到新矩陣中的對應位置。在遍歷完成后,再將新矩陣中復制到原矩陣即可。下面我們來看一下代碼實現。

class Solution(object):
    def rotate(self, matrix):
        """
        :type matrix: List[List[int]]
        :rtype: None Do not return anything, modify matrix in-place instead.
        """
        #矩陣的大小
        n = len(matrix)
        #申請一個輔助矩陣
        temp = [[0] * n for _ in range(n)]
        #遍歷矩陣中的所有元素,放到輔助矩陣的相應位置中
        for i in range(n):
            for j in range(n):
                temp[j][n - i - 1] = matrix[i][j]
                
        #將輔助矩陣復制給矩陣
        matrix[:] = temp

該算法的時間復雜度是O(N2),空間復雜度O(N2)。

進階

那我們如何在不使用輔助空間的情況下,實現矩陣的原地旋轉呢?我們來看一下方法一中為什么要引入輔助空間,對於matrix中的元素,我們使用公式temp[j] [n - i - 1] = matrix[i] [j]進行旋轉,如果不申請輔助矩陣,我們直接把元素 matrix[i] [j],放到矩陣 matrix[j] [n - i - 1]位置,原矩陣中的matrix[j] [n - i - 1]元素就被覆蓋了,這顯然不是我們要的結果。

當知道了如何原地旋轉矩陣之后,這里還有一點需要明確:我們應該選取哪些位置進行上述的原地交換操作呢?通過上面的分析可以知道,一次可以原地交換四個位置,所以:

  1. 當n為偶數時,我們需要選取 n^2 / 4 = (n/2) * (n/2)個元素進行原地交換操作,可以將該圖形分為四塊,可以保證不重復、不遺漏旋轉所有元素;
  2. 當n為奇數時,由於中心的位置經過旋轉后位置不變,我們需要選取 (n^2-1)/4=(n-1)/2 * (n+1) /2個元素進行原地交換操作,我們以5*5的矩陣為例,可以按照以下方式划分,進而保證不重復、不遺漏的旋轉所有元素。

下面我們來看一下代碼的實現。

class Solution(object):
    def rotate(self, matrix):
        #矩陣的大小
        n = len(matrix)
        for i in range(n // 2):
            for j in range((n + 1) // 2):
                #進行一輪原地旋轉,旋轉4個元素
                temp = matrix[i][j]
                matrix[i][j] = matrix[n - j - 1][i]
                matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1]
                matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1]
                matrix[j][n - i - 1] = temp

該算法的時間復雜度是O(n^2),空間復雜度是O(1)。

數組中的最長連續子序列

問題描述

LeetCode128. 最長連續序列

給定無序數組arr,返回其中最長的連續子序列的長度(要求值連續,位置可以不連續,例如 3,4,5,6為連續的自然數)。請你設計並實現時間復雜度為 O(n) 的算法解決此問題。

示例:

輸入:[100,4,200,1,3,2]

輸出:4

說明:最長數字連續序列是 [1, 2, 3, 4]。它的長度為 4。

分析問題

因為給定的數組是無序的,所以我們最直觀的想法就是遍歷數組中的每個元素,然后考慮以其為起點,不斷的在數組中尋找x+1,x+2,...,x+n是否存在,如果最長匹配到了x+n,那么就表明以x為起點的最長連續序列為x,x+1,x+2,...,x+n,其長度為n+1。因為在數組中尋找一個數是否存在,是需要O(n)的時間復雜度;而在哈希表中判斷一個數是否存在只需要O(1)的時間復雜度,所以我們可以通過引入一個哈希表,來減低該算法的時間復雜度。

在最壞的情況下,該算法的時間復雜度是O(n^2)(即外層需要枚舉n個數,內層也需要匹配n次),無法滿足題目的要求,那我們如何來進行優化呢?

我們來分析一下算法的執行過程,如果我們已經知道了數組中存在有一個x,x+1,x+2,...,x+n的連續序列,那么我們就沒有必要再繼續以x+1,x+2,....,x+n為起點去數組中尋找連續序列了,因為得到的結果肯定不會優於以x為起點的連續序列。所以,我們在外層循環中如果碰到這種情況直接跳過就好。

具體做法是,我們在遍歷的元素x時,去判讀其前驅數x-1是否存在,如果存在的話,就不需要執行后面的邏輯,因為從x-1進行匹配的結果是優於從x進行匹配的,所以跳過x。

下面我們來看一下代碼的實現。

class Solution:
    def longestConsecutive(self, nums):
        #記錄最長子序列的長度
        length = 0

        #將數組中的元素放入set中
        num_set = set(nums)

        for num in num_set:
            #判斷num-1是否存在於哈希表中,如果存在,直接跳過
            if num - 1 not in num_set:
                currentdata = num
                currentlength = 1
                #繼續尋找
                while currentdata + 1 in num_set:
                    currentdata += 1
                    currentlength += 1
                #取最大值
                length = max(currentlength, length)

        return length

該算法的時間復雜度是O(n),空間復雜度也是O(n)。

尋找峰值

問題描述

LeetCode 162. 尋找峰值

峰值元素是指其值嚴格大於左右相鄰值的元素。給你一個整數數組 nums,找到峰值元素並返回其索引。數組可能包含多個峰值,在這種情況下,返回 任何一個峰值 所在位置即可。你可以假設 nums[-1] = nums[n] = -∞ 。你必須實現時間復雜度為 O(log n) 的算法來解決此問題。

示例:

輸入:nums = [1,2,3,1]

輸出:3 是峰值元素,你的函數應該返回其索引 2。

分析問題

峰值是指其值嚴格大於左右相鄰值的元素,那么很顯然數組中的最大值一定是一個峰值,因為它肯定大於它左右兩側的元素。所以,我們可以對數組進行一次遍歷,然后求出其最大值的位置即可。該算法的時間復雜度是O(n),是不滿足題目要求的。那我們如何進行優化呢。

我們可以這么來考慮,如果我們從一個位置開始,不斷地向高處走,那么最終一定可以到達一個峰值位置。

因此,我們首先在 [0, n) 的范圍內隨機一個初始位置 i,隨后根據nums[i-1],nums[i],nums[i+1]三者的關系決定向哪個方向走。

  • 如何nums[i-1] < nums[i] > nums[i+1],那么位置i就是峰值的位置,我們直接返回i。
  • 如果nums[i-1] < nums [i] < nums[i+1] , 那么位置i處於上坡位置,要想找到峰值,需要往右走,即i=i+1。
  • 如果nums[i-1] > nums[i] > nums[i+1],那么位置i處於下坡位置,要想找到峰值,需要往左走,季i=i-1。
  • 如果nums[i-1] > nums[i] < nums[i+1],此時i處於坡底,要想找到峰值,兩個方向都可以,我們假設這種情況需要往右邊走。

綜上所述,當i不是峰值時。

  • 如果nums[i] > nums[i+1] , i需要往左走,即執行i=i-1。
  • 如果nums[i] < nums[i+1],i需要往右走,即執行i=i+1。
import random
class Solution:
    def findPeakElement(self, nums):
        #數組的長度
        n = len(nums)
        #隨機初始化一個位置
        idx = random.randint(0, n - 1)

        
        #方便處理nums[-1] 以及 nums[n]的邊界情況
        def get_value(i):
            if i == -1 or i == n:
                return float('-inf')
            return nums[i]
        #當i不是峰值時,如果nums[i] < nums[i+1],此時需要向右走,即i=i+1
        #否則需要向左走,即i=i-1
        while not (get_value(idx - 1) < get_value(idx) > get_value(idx + 1)):
            if get_value(idx) < get_value(idx + 1):
                idx = idx + 1
            else:
                idx = idx - 1
                
        return idx

在最壞的情況下,假設nums是單調遞增的,並且我們是從0開始出發的,那這樣就需要一直向右走到數組的最后一個位置,該算法的時間復雜度是O(n),而題目要求的時間復雜度是O(logn),顯然是不符合的,那我們該如何求解呢?對於像O(logn)這種形式的時間復雜度,我們最先想到的就是二分法,但是數組中的元素又不是排序的,那我們該如何使用二分法來求解此題呢?下面我們就來看一下。

通過分析,我們可以發現,當nums[i] < nums[i+1]時,我們需要讓i向右走,即執行i=i+1,那么i和i左邊的所有位置在后續的迭代中是永遠不會走到的。因為假設此時在i+1的位置,要想向左走到位置i,就需要nums[i] > nums[i+1],顯然是不可能的。所以我們可以設計如下算法,首先創建兩個變量l、r表示可走的左、右邊界,開始時l=0,r=n-1。

  1. 取區間[l,r]的中點,即mid=(l+r)/2。
  2. 如果下標mid是峰值,直接返回。
  3. 如果nums[mid] < nums[mid+1],表示峰值在mid的右邊,所以拋棄區間[l,mid],在剩余的[mid+1,r]的區間內去尋找。
  4. 如果nums[mid] > nums[mid+1],表示峰值在mid的左邊,所以拋棄區間[mid, r],在剩余的[l,mid-1]的區間內去尋找。

這樣的話,該算法每次淘汰掉一半的元素,所以時間復雜度是O(logn)。

下面我們來看一下代碼的實現。

class Solution:
    def findPeakElement(self, nums):
        n = len(nums)
        # 方便處理 nums[-1] 以及 nums[n] 的邊界情況
        def get_value(i):
            if i == -1 or i == n:
                return float('-inf')
            return nums[i]

        #l,r代表區間的左右邊界
        l=0
        r=n-1
        ans=-1
        while l <= r:
            #取中點
            mid = (l + r) // 2
            #如果是峰值,直接返回
            if get_value(mid - 1) < get_value(mid) > get_value(mid + 1):
                ans = mid
                break
            #如果nums[mid]<nums[mid+1],代表峰值在[mid+1,r]
            #否則在區間[l,mid-1]
            if get_value(mid) < get_value(mid + 1):
                l = mid + 1
            else:
                r = mid - 1

        return ans

二維數組中的查找

問題描述

LeetCode 劍指 Offer 04. 二維數組中的查找

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

示例:

現有矩陣 matrix 如下:

[
  [1, 2, 8, 9],
  [2, 4, 9, 12],
  [4, 7, 10, 13],
  [6, 8, 11, 15]
]

給定 target = 9,返回 true

給定 target = 20,返回 false

分析問題

在二維數組中去查找一個元素,我們可以遍歷一遍二維數組中的每一個元素,然后去判斷是否和目標值相同。該算法的時間復雜度是O(m*n),它顯然不是最優的解法。我們接下來來看一下如何進行優化。

因為給定的數組中的每一行、每一列都是遞增排列的。當我們以左下角為起點時,只有向上和向右兩種選擇,上邊的值嚴格的小,右邊的值嚴格的大。所以,我們可以利用這個性質。

我們從二維數組的左下角為起點,開始遍歷數組。

  • 當元素matrix [i] [j] < target,說明target在matrix [i] [j]的右方,所以向右走,即執行 j=j+1。
  • 當元素matrix [i] [j] > target,說明target在matrix [i] [j]的上方,所以向上走,即執行 i= i-1。
  • 當元素matrix [i] [j] == target,代表已經找到了目標值,直接返回true即可。

最后,當超出二維數組的邊界時,表示數組中不存在該元素,直接返回false。

下面我們來看一下代碼的實現。

class Solution(object):
    def findNumberIn2DArray(self, matrix, target):
        """
        :type matrix: List[List[int]]
        :type target: int
        :rtype: bool
        """
        m=len(matrix)
        #matrix為空時,直接返回False
        if m==0:
            return False
        n=len(matrix[0])
        #從左下角開始遍歷
        i = m - 1
        j = 0
        while i >= 0 and j <= n - 1:
            # 相等返回True
            if matrix[i][j] == target:
                return True
            # 大於向上走
            elif matrix[i][j] > target:
                i = i - 1
            # 小於向右走
            elif matrix[i][j] < target:
                j = j + 1
        return False

該算法的時間復雜度是O(m+n),空間復雜度是O(1)。

數組中的逆序對

問題描述

LeetCode 劍指 Offer 51. 數組中的逆序對

在數組中的兩個數字,如果前面一個數字大於后面的數字,則這兩個數字組成一個逆序對。輸入一個數組,求出這個數組中的逆序對的總數P。

示例:

輸入:[7,5,6,4]

輸出:5

分析問題

這道題最容易想到的就是暴力解法,即使用兩層for循環,去逐一判斷是否構成逆序關系。

class Solution:
    def reversePairs(self, nums) :
        n = len(nums)
        #如果數組中的元素的個數
        # 為0或者1時,表示沒有逆序對,直接返回0
        if n < 2:
            return 0
        #逆序對的個數
        res = 0
        #兩層循環去逐一判斷是否是逆序對
        for i in range(0,  n - 1):
            for j in range(i + 1, n):
                if nums[i] > nums[j]:
                    res += 1
        return res

該算法的時間復雜度是O(n^2),空間復雜度是O(1)。

該算法顯然不是最優解,我們第一次聽說逆序對的這個概念,應該是在數組排序時。所以,我們這里可以采用歸並排序算法來求解逆序對的個數。

首先,我們先來回顧一下什么是歸並排序。歸並排序是分治思想的典型應用,它包含以下三個步驟:

  1. 分解:假設待排序的區間為[l,r],我們令mid=(l+r)//2,將區間分成[l,mid]和[mid+1,r]兩部分。
  2. 解決:使用歸並排序遞歸的求解兩個子序列,使其有序。
  3. 合並:把兩個已經排好序的子序列[l,mid]和[mid+1,r]合並起來。

下面我們來看一下如何使用歸並排序來求解逆序對?關鍵在於歸並排序的合並步驟,即利用數組的部分有序性,一下子計算出一個數之前或者之后的逆序數的個數;下面我們來看一個具體的例子,假設目前有兩個已經排好序的序列等待合並,分別是L=[8,17,30,45] 和 R=[10,24,27,39],如下圖所示。

我們來看一下如何把L、R合並成一個有序的數組。整體思路是將原數組拷貝到輔助數組,再使用雙指針法,每次將較小的元素歸並回去。

下面我們來看一下代碼的實現。

class Solution:
    def reversePairs(self, nums) :
         n = len(nums)
         #數組中個數小於2時,不存在逆序對,所以返回0
         if n < 2:
            return 0

         #用於歸並的輔助數組
         temp = [0 for _ in range(n)]
         return self.reverse_pairs(nums, 0, n - 1, temp)

    #歸並排序nums
    def reverse_pairs(self, nums, left, right, temp):
        if left == right:
            return 0
        mid = (left + right)//2
        #將nums分成左、右兩部分,遞歸求解
        left_pairs = self.reverse_pairs(nums, left, mid, temp)
        right_pairs = self.reverse_pairs(nums, mid + 1, right, temp)

        #子序列[left, mid] 和 [mid + 1, right] 已經完成了排序並且計算好逆序對
        reverse_pairs = left_pairs + right_pairs
        #nums[mid] <= nums[mid + 1],此時[left,right]已經是有序的了
        #所以不存在橫跨兩個區間的逆序對,直接返回reverse_pairs即可
        if nums[mid] <= nums[mid + 1]:
            return reverse_pairs

        #計算跨兩個區間的逆序對
        cross_pairs = self.merge_and_count(nums, left, mid, right, temp)

        return reverse_pairs + cross_pairs

    def merge_and_count(self, nums, left, mid, right, temp):
        """
        [left, mid] 有序,[mid + 1, right] 有序,將兩個有序的子序列合並成一個有序的子序列
        """
        #將nums的元素copy到輔助數組中
        for i in range(left, right + 1):
            temp[i] = nums[i]

        i = left
        j = mid + 1
        res = 0
        for k in range(left, right + 1):
            #i>mid,說明left部分已經遍歷完,直接將right插入nums
            if i > mid:
                nums[k] = temp[j]
                j += 1
            #j>right,說明right部分已經遍歷完,直接將left插入nums
            elif j > right:
                nums[k] = temp[i]
                i += 1
            # 此時left數組元素小,插入nums中,不計算逆序數
            elif temp[i] <= temp[j]:
                nums[k] = temp[i]
                i += 1
            # 此時right數組元素小,插入nums中,統計逆序對,
            # 一次可以統計出一個區間的個數的逆序對
            else:
                nums[k] = temp[j]
                j += 1
                res += (mid - i + 1)
        return res

該算法的時間復雜度是O(nlogn),空間復雜度是O(1)。

旋轉數組

問題描述

LeetCode189. 旋轉數組

一個數組A中存有 n 個整數,在不允許使用另外數組的前提下,將每個整數循環向右移 M( M >=0)個位置,即將A中的數據由(A0 A1……An-1 )變換為(An-m…… An-1 A0 A1……An-m-1)(最后 M 個數循環移至最前面的 M 個位置)。

示例:

輸入:[1,2,3,4,5,6,7]

輸出:[5,6,7,1,2,3,4]

分析問題

這道題最直觀的想法就是使用額外的數組來將每個元素放到正確的位置。我們用n來表示數組的長度,然后遍歷原數組,將原數組下標為i的元素放至新數組下標為 (i+k) % n 的位置,最后將新數組拷貝到原數組即可。

class Solution:
    def rotate(self,nums,k):
        n=len(nums)
        tmp=[0]*n

        for i in range(0,n):
            #將數組nums[i]放到新數組的相應位置
            tmp[(i+k)%n]=nums[i]
        #將新數組拷貝到原數組
        nums[:]=tmp[:]

該算法的時間復雜度是O(n),空間復雜度也是O(n)。但是題目要求不允許使用另外的數組,顯然該算法是不符合題意的。我們來觀察一下數組移動前后的變化,當我們將數組的元素向右移動k次后,尾部 k mod n個元素會移動至數組的頭部,其余元素向后移動k mod n 個位置。因此我們可以采用數組翻轉的方法來求解。具體思路如下:首先我們將所有元素進行翻轉,這樣尾部k mod n個元素就被移動到數組的頭部。然后我們再翻轉 [0, (k mod n)-1] 區間的元素和 [k mod n, n-1]區間的元素,即能得到最后的答案。

下面我們來看一下代碼的實現。

class Solution:
    #對數組中的元素進行翻轉
    def reverse(self,nums,start,end):
        while start < end:
            tmp=nums[start]
            nums[start]=nums[end]
            nums[end]=tmp
            start=start+1
            end=end-1
    def rotate(self,nums,k):
        n=len(nums)
        k=k%n
        #對數組進行反轉
        self.reverse(nums,0,n-1)
        #對區間nums[0,k-1]再進行翻轉
        self.reverse(nums,0,k-1)
        #對區間nums[k,n-1]再進行翻轉
        self.reverse(nums,k,n-1)

該算法的時間復雜度是O(n),空間復雜度是O(1)。

調整數組順序使奇數位於偶數前面

問題描述

輸入一個整數數組,實現一個函數來調整該數組中數字的順序,使得所有奇數在數組的前半部分,所有偶數在數組的后半部分。

示例:

輸入:[1,2,3,4]

輸出:[1,3,2,4]

分析問題

這道題我們可以使用雙指針法來求解,具體思路如下。

  1. 首先,申請兩個指針i和j,分別指向數組nums的左右兩端,即i=0,j=n-1。
  2. 當i所指的位置為奇數時,執行i=i+1,直到遇到偶數。
  3. 當j所指的位置為偶數時,執行j=j-1,直到遇到奇數。
  4. 然后交換nums[i]和nums[j]的值
  5. 重復上述操作,直到i==j為止。

下面我們來看一下代碼的實現。

class Solution(object):
    def exchange(self, nums):
        #申請兩個變量i和j,開始時,指向數組的兩端
        i=0
        j=len(nums)-1
        while i < j:
            #從i開始從左向右尋找,直到找到第一個偶數
            while i < j and nums[i] % 2 == 1:
                i = i + 1
            #從j開始從右想左尋找,直到找到第一個奇數
            while i < j and nums[j] % 2 == 0:
                j = j - 1
            nums[i], nums[j] = nums[j], nums[i]
        return nums

其實這道題我們還可以使用快慢指針法來求解,首先我們定義兩個指針fast和slow,fast的作用是向前搜索奇數所在的位置,slow的作用是指向下一個奇數應當存放的位置。在fast向前移動的過程中,當它搜索到奇數時,將它和nums[slow]進行交換,然后讓slow向前移動一個位置,重復上述操作,直到fast指向數組的末尾為止。

class Solution:
    def exchange(self, nums):
        slow = 0
        fast = 0
        #循環遍歷,直到fast指向nums的末尾
        while fast < len(nums):
            #如果fast指向奇數,
            #交換nums[slow]和nums[fast]
            if nums[fast] % 2 == 1:
                nums[slow], nums[fast] = nums[fast], nums[slow]
                slow=slow+1
            fast=fast+1
        return nums

該算法的時間復雜度是O(n),空間復雜度是O(1)。

矩陣乘法

問題描述

給定兩個n * n 的矩陣A和B,求A * B。

示例:

輸入:[[1,2],[3,2]],[[3,4],[2,1]]

輸出:[[7,6],[13,14]]

分析問題

我們可以使用矩陣乘法規則來求解。對於n * n的矩陣A和矩陣B相乘,所得矩陣C的第i行第j列的元素可以表示為Ci,j=Ai,1* B1,j + Ai,2 * B2,j + ... + Ai,n * Bn,j , 即等於A的第i行和B的第j列對應元素的乘積之和。

class Solution:
    def solve(self , a, b):
        # write code here
        #矩陣a和矩陣b是n*n的矩陣
        n=len(a)
        res=[[0] * n for _ in range(n)]

        for i in range(0,n):
            for j in range(0,n):
                for k in range(0,n):
                    #C的第i行第j列的元素為
                    #A的第i行和B的第j列對應元素乘積的和
                    res[i][j] += a[i][k]*b[k][j]
        return res

該算法的時間復雜度是O(N3),空間復雜度是O(N2)。

我們都知道對於二維數組來說,在計算機的內存中實際上是順序存儲的,如下所示:

因為操作系統加載數據到緩存中時,都是把命中數據附近的一批數據一起加載到緩存中,因為操作系統認為如果一個內存位置被引用了,那么程序很可能在不久的未來引用附近的一個內存位置。所以我們通過調整數組的讀取順序來進行優化,使得矩陣A和B順序讀取,然后相繼送入CPU中進行計算,最后使得運行時間能夠更快。下面我們來看一下具體做法:

class Solution:
    def solve(self , a, b):
        # write code here
        #矩陣a和矩陣b是n*n的矩陣
        n=len(a)
        res=[[0] * n for _ in range(n)]

        for i in range(0,n):
            for j in range(0,n):
                #順序訪問矩陣A的元素
                temp=a[i][j]

                for k in range(0,n):
                    #矩陣b的元素也是順序訪問的
                    res[i][k] += temp * b[j][k]

        return res

該算法的時間復雜度是O(N3),但是該算法利用了緩存優化,順序讀取數組A和數組B中的元素,因此一般會比第一種方法運行更快。該算法的空間復雜度是O(N2)。

數字在升序數組中出現的次數

問題描述

給定一個長度為 n 的非降序數組和一個非負數整數 k ,要求統計 k 在數組中出現的次數。

示例:

輸入:[1,2,3,3,3,3,4,5],3

輸出:4

分析問題

不管數組是否有序,如果要查找數組中是否存在某個元素,我們只需要遍歷一般數組就好。

class Solution:
    def GetNumberOfK(self,data, k):
        n=0
        for x in data:
            if x==k:
               n=n+1
        return n

該算法的時間復雜度是O(n),空間復雜度是O(1)。

因為題目給定的數組是有序的,所以我們可以使用二分查找來求解。對於有序的數組,如果要尋找的目標值target有多個,那么他們肯定是連在一起的,所以我們可以通過二次二分查找,分別尋找目標值所在范圍的上界和下界。首先,我們來看一下上界和下界的定義。

  • 下界定義為:如果存在目標值,則指向第一個目標值;如果不存在, 則指向大於目標值的第一個值。
  • 上界定義為:不管目標值存在與否,都指向大於目標值的第一個值。

下面我們來看一下代碼實現。

class Solution:
    def GetNumberOfK(self,data, k):
        l=0
        r=len(data)-1

        #二分法尋找下界
        while l<r:
            mid = (r+l) // 2
            if data[mid] < k:
                l = mid + 1
            else:
                r = mid

        left=l
        #尋找上界
        l = 0
        r = len(data)-1
        while l<r:
            mid=(r+l)//2
            if data[mid] <= k:
                l=mid+1
            else:
                r=mid

        right=l
        return right - left

該算法的時間復雜度是O(logN),空間復雜度是O(1)。

三個數的最大乘積

問題描述

LeetCode628. 三個數的最大乘積

給定一個長度為 n 的無序數組 A ,包含正數、負數和 0 ,請從中找出 3 個數,使得乘積最大,返回這個乘積。

示例:

輸入:nums = [1,2,3,4]

輸出:24

分析問題

數組中三個數的最大乘積有以下二種情況。

  1. 如果數組中的元素全是非負數或者非正數,那么數組中最大的三個數相乘就是最大乘積。
  2. 如果數組中的元素既有正數也有負數,那么最大的乘積既可能是三個最大正數的乘積,也可能是兩個最小負數(絕對值最大)和最大正數的乘積。

所以,我們只需要找出數組中最大的三個數以及最小的兩個數,就可以求得結果。下面我們來看一下如何求解。最容易想到的方式就是先對數組進行降序排序,排好序的數組的前三位以及后兩位就是要找的最大的三個數以及最小的兩個數。

class Solution:
    def maximumProduct(self,nums):
        nums=sorted(nums)
        n=len(nums)
        return max(nums[0] * nums[1] * nums[n-1], nums[n - 3] * nums[n - 2] * nums[n-1])

該算法的時間復雜度是O(nlogn),其中n為數組的長度。排序需要O(nlogn)的時間。

空間復雜度是O(logn),主要是排序的開銷。

其實我們也可以掃描一遍數組,就可以求出這五個數,如下所示。

import sys
class Solution:
    def maximumProduct(self,nums):
        #最小和第二小
        min1=min2=sys.maxsize
        #最大、第二大、第三大
        max1=max2=max3=-sys.maxsize-1
        for x in nums:
            if x < min1:
                min2=min1
                min1=x
            elif x<min2:
                min2=x

            if x>max1:
                max3=max2
                max2=max1
                max1=x
            elif x>max2:
                max3=max2
                max2=x
            elif x>max3:
                max3=x

        return max(max1*max2*max3,max1*min1*min2)

該算法的時間復雜度是O(n),空間復雜度是O(1)。

最后

原創不易!各位小伙伴覺得文章不錯的話,不妨點贊(在看)、留言、轉發三連走起!

你知道的越多,你的思維越開闊。我們下期再見。


免責聲明!

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



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