數組是一種線性表結構,用一段連續的內存存儲相同數據類型的數據。
時間複雜度:因爲在內存中是連續存儲的,所以利用尋址公式,它支持時間複雜度爲O(1)的隨機訪問操作,但是插入和刪除的操作因爲涉及到數據搬移,所以平均情況時間複雜度爲 O(n)
在使用數組時,要警惕數組越界,特別在C語言中訪問越界的數據也不會報錯。但是像Python、Java語言則會做越界檢查。
下面通過一些Leetcode上有關數組的操作的題目,來掌握數組相關的算法。
1、合併兩個有序整數數組
from typing import List
class Merge:
"""
給定兩個有序整數數組 nums1 和 nums2,將 nums2 合併到 nums1 中,使得 num1 成爲一個有序數組。
nums1 有足夠的空間(空間大小大於或等於 m + n)來保存 nums2 中的元素
https://leetcode-cn.com/problems/merge-sorted-array/
"""
@staticmethod
def merge_from_head_to_tail(nums1: List[int], m: int, nums2: List[int], n: int) -> None:
"""
雙指針法,從前往後。
將指針p1 置爲 nums1的開頭, p2爲 nums2的開頭,在每一步將最小值放入輸出數組中。
由於 nums1 是用於輸出的數組,需要將nums1中的前m個元素放在其他地方,也就需要 O(m)的空間複雜度。
:param nums1:
:param m: nums1的有效數據個數
:param nums2: 將 nums2 合併到 nums1 中
:param n: nums2的有效數據個數
:return: None
"""
nums1_copy = nums1[:m] # 複製nums1中的有效數據到nums1_copy,空間複雜度:O(m)
i = 0 # 表示合併前nums1_copy的下標
j = 0 # 表示合併前nums2的下標
for k in range(m + n): # k表示合併後的數組的下標,m是nums1的有效元素個數,n表示nums2的有效元素個數。時間複雜度是O(m+n)
# 注意:要把 nums1_copy 和 nums2 歸併完成的邏輯寫在前面,否則會出現數組下標越界異常
if i == m: # nums3已經遍歷完了
nums1[k] = nums2[j] # 將nums2中的剩餘元素陸續加到nums1中
j = j + 1
elif j == n: # nums2遍歷完了
nums1[k] = nums1_copy[i] # 將nums3中的剩餘元素陸續加到nums1中
i = i + 1
elif nums2[j] > nums1_copy[i]:
nums1[k] = nums1_copy[i] # 將num2和num3中較小的數放入到nums1中
i = i + 1
else:
nums1[k] = nums2[j]
j = j + 1
@staticmethod
def merge_from_tail_to_head(nums1: List[int], m: int, nums2: List[int], n: int) -> None:
"""
雙指針 / 從後往前
上面的方法需要使用額外空間。這是由於在從頭改變nums1的值時,需要把nums1中的元素存放在其他位置。
如果從結尾開始改寫 nums1 的值,則不需要額外的空間,因爲nums1的尾部是沒有數據的。
:param nums1: nums1 有足夠的空間(空間大小大於或等於 m + n)來保存 nums2 中的元素
:param m: nums1的有效數據個數
:param nums2: 將 nums2 合併到 nums1 中
:param n: nums2的有效數據個數
:return: None
"""
i = m - 1 # nums1的下標
j = n - 1 # nums2的下標
for k in range(m + n - 1, -1, -1): # 時間複雜度是O(M+N),空間複雜度是O(1)
if i == -1: # nums1的有效數據遍歷完了,將nums2逐個加入到nums1中
nums1[k] = nums2[j]
j = j - 1
elif j == -1: # nums2遍歷完了,那就可以結束了
break
elif nums2[j] > nums1[i]: # 找nums1和nums2中的大者,放入nums1
nums1[k] = nums2[j]
j = j - 1
else:
nums1[k] = nums1[i]
i = i - 1
2、旋轉數組
class Rotate(object):
"""
給定一個數組,將數組中的元素向右移動 k 個位置,其中 k 是非負數。
舉例:
輸入: [1,2,3,4,5,6,7] 和 k = 3
輸出: [5,6,7,1,2,3,4]
"""
@staticmethod
def rotate_array_by_raw(nums: List[int], k: int) -> None:
"""
時間複雜度:O(n*k) 。每個元素都被移動 1 步(複雜度是O(n))經過k次循環(複雜度是O(k)),乘法法則時間複雜度是O(n*k);不推薦這種辦法。
空間複雜度是O(1)
"""
for i in range(k):
last = nums[-1]
for j in range(len(nums) - 1, 0, -1):
nums[j] = nums[j - 1]
nums[0] = last
@staticmethod
def rotate_array_by_join(nums: List[int], k: int) -> None:
"""
時間複雜度:O(n);空間複雜度:O(1),沒有使用額外的空間。
"""
k = k % len(nums)
nums[:] = nums[-k:] + nums[:-k]
@staticmethod
def rotate_array_by_rotate(nums: List[int], k: int) -> None:
"""
翻轉三次。
時間複雜度:O(n);空間複雜度:O(1),沒有使用額外的空間。
"""
k = k % len(nums)
nums[:] = nums[::-1]
nums[:k] = nums[:k][::-1]
nums[k:] = nums[k:][::-1]
3、尋找中心索引
def find_pivot_index(nums: List[int]) -> int:
"""
請編寫一個能夠返回數組“中心索引”的方法。
我們是這樣定義數組中心索引的:數組中心索引的左側所有元素相加的和等於右側所有元素相加的和。
如果數組不存在中心索引,那麼我們應該返回 -1。如果數組有多箇中心索引,那麼我們應該返回最靠近左邊的那一個。
:param nums:整型數組
:return:中心索引
舉例:
輸入: nums = [1, 7, 3, 6, 5, 6]
輸出: 3
時間複雜度:O(N),其中 N 是 nums 的長度。
空間複雜度:O(1),使用了 _sum 和 left_of_sum 。
"""
sum_of_left = 0
_sum = sum(nums)
for j in range(len(nums)): # 用enumerate也可以
sum_of_right = _sum - sum_of_left - nums[j]
if sum_of_left == sum_of_right:
return j
sum_of_left = sum_of_left + nums[j]
return -1
4、數組最小元素的最大和
def array_pair_sum(nums: List[int]) -> int:
"""
給定長度爲 2n 的數組, 將這些數分成 n 對, 例如 (a1, b1), (a2, b2), ..., (an, bn) ,使得從1 到 n 的 min(ai, bi) 總和最大。
舉例:
輸入: [1,4,3,2]
輸出: 4
解釋: n 等於 2, 最大總和爲 4 = min(1, 2) + min(3, 4)。
思路:每對組合的兩個元素之差最小,將會導致總和最大。可以對數組進行從小到大排序,然後兩兩組合配對,這樣的配對導致每個組合中的差最小,從而導致所需要的總和最大。
"""
nums.sort()
_sum = 0
for i in range(0, len(nums), 2):
_sum = _sum + nums[i]
# _sum = sum(nums[0::2]) # 效率更高
return _sum # 排序後的奇數位置元素之和。
5、數組形式的整數加法
class Solution:
"""
對於非負整數 X 而言,X 的數組形式是每位數字按從左到右的順序形成的數組。例如,如果 X = 1231,那麼其數組形式爲 [1,2,3,1]。
給定非負整數 X 的數組形式 nums,返回整數 X+k 的數組形式。
"""
def add_to_array_form(self, nums: List[int], k: int) -> List[int]:
"""
先把數組轉成字符串,再轉成整數,與K做和之後,轉成字符串,再將字符串轉成數組
"""
return [int(j) for j in str(int("".join([str(i) for i in nums])) + k)]
6、刪除數組中的重複元素
class RemoveDuplicate:
"""
給定一個排序數組,你需要在原地刪除重複出現的元素,使得每個元素只出現一次,返回移除後數組的新長度。要求在O(1)的空間複雜度內完成。
"""
@staticmethod
def remove_duplicate_1(nums: list) -> int:
for i in range(len(nums) - 1, 0, -1):
if nums[i] == nums[i - 1]:
nums.pop(i)
return len(nums) # list(set(nums))
@staticmethod
def remove_duplicate_2(nums: list) -> int:
"""
雙指針法,慢指針i,快指針j,都從0開始。當快指針到達尾部時,慢指針i就是不重複數組的最後一個下標。
只要nums[i]=nums[j],我們就增加 j 以跳過重複項。
遇到 nums[j]!=nums[i],慢指針加1,將nums[j]賦值給nums[i]
:param nums:
:return:
"""
length = len(nums)
if length <= 1:
return length
i = 0
for j in range(length):
if nums[i] != nums[j]:
i = i + 1
nums[i] = nums[j]
return i + 1
7、按奇偶排序數組
class Sort:
"""
給定一個非負整數數組 nums,返回一個數組,在該數組中, nums 的所有偶數元素之後跟着所有奇數元素。
舉例:
輸入:[3,1,2,4]
輸出:[2,4,3,1]
輸出 [4,2,3,1],[2,4,1,3] 和 [4,2,1,3] 也會被接受。
"""
@staticmethod
def sort_array_by_parity_1(nums: List[int]) -> List[int]:
"""
時間複雜度:O(N),其中 N 是 nums 的長度。空間複雜度:O(N),存儲結果的數組。
"""
return [x for x in nums if x % 2 == 0] + [y for y in nums if y % 2 == 1]
@staticmethod
def sort_array_by_parity_2(nums: List[int]) -> List[int]:
"""
時間複雜度:O(N*logN),其中 N 是 nums 的長度。空間複雜度:排序空間爲 O(N),取決於內置的 sort 函數實現。
"""
nums.sort(key=lambda x: x % 2)
return nums
@staticmethod
def sort_array_by_parity_3(nums: List[int]) -> List[int]:
"""
如果希望原地排序,可以使用快排,一個經典的算法。
維護兩個指針i和j,分別指向數組的兩端。時刻保證i左邊的都是偶數(nums[i]%2=0),j右邊的都是奇數(nums[j]%2=1)。
時間複雜度是空間複雜度:排序空間爲 O(1)
"""
i, j = 0, len(nums) - 1
while i < j:
if nums[i] % 2 > nums[j] % 2: # i指向奇數j指向偶數,則交換它們
nums[i], nums[j] = nums[j], nums[i]
if nums[i] % 2 == 0: # i指向偶數,i向右移動1位
i = i + 1
if nums[j] % 2 == 1: # j指向奇數,j向左移動1位
j = j - 1
return nums
8、容器能否完全替代數組?
針對數組類型,很多語言都提供了容器類,比如 Java 中的 ArrayList,Python中的List。
ArrayList 或者 List這些容器類,最大的優勢就是可以將很多數組操作的細節封裝起來。比如數組插入、刪除數據時需要搬移其他數據等。
另外,它還有一個優勢,就是支持動態擴容,但是擴容的操作是有開銷的,如果我們事先能確認數組的大小,最好在創建 ArrayList 的時候事先指定數據大小。
在項目開發中,什麼時候適合用數組,什麼時候適合用容器呢?對於業務開發,大部分情況下,直接使用容器就足夠了,省時省力。但是追求極致性能、非常底層的應用開發時,用數組更合適。