測試開發基礎之算法(2):數組的特點及相關算法

數組是一種線性表結構,用一段連續的內存存儲相同數據類型的數據。

時間複雜度:因爲在內存中是連續存儲的,所以利用尋址公式,它支持時間複雜度爲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 的時候事先指定數據大小。
在項目開發中,什麼時候適合用數組,什麼時候適合用容器呢?對於業務開發,大部分情況下,直接使用容器就足夠了,省時省力。但是追求極致性能、非常底層的應用開發時,用數組更合適。

9、參考資料

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章