2023.9Datawhale_Leetcode_算法入门与数组_学习笔记(三)

练习题

把数组排成最小的数

        给定一个非负整数数组 nums ,将数组中的数字拼接起来排成一个数,打印能拼接出的所有数字中的最小的一个。

class Solution:
    def minNumber(self, nums: List[int]) -> str:

        # 元素转字符串
        nums = [str(x) for x in nums]

        # 定义新的排序判断机制
        def fun(a, b):
            if int(a+b) > int(b+a):
                return True
            else:
                return False

        # 改冒泡排序:元素大小相当于泡泡大小
        # 设置最大冒泡次数
        for i in range(len(nums)-1):
            # 设置停止标志
            flag = False
            # 每次判定,指针左移1个单位,有序区间与无序区间的界限左移
            for j in range(len(nums) - i - 1):
                # 大泡泡向右上浮,小泡泡向左下沉,表现为元素交换
                if fun(nums[j], nums[j + 1]):
                    nums[j], nums[j + 1] = nums[j + 1], nums[j]
                    flag = True
            # 不再有泡泡上浮即停    
            if not flag:
                break

        # 连接元素
        return ''.join(nums)

        这题的主要思路是把整数的位数对比的问题,转化为拼接后整体比较的问题。通过字符串的简单操作和冒泡排序即可实现。

class Solution:
    def minNumber(self, nums: List[int]) -> str:
        def quick_sort(l , r):
            if l >= r: return
            i, j = l, r
            while i < j:
                while strs[j] + strs[l] >= strs[l] + strs[j] and i < j: j -= 1
                while strs[i] + strs[l] <= strs[l] + strs[i] and i < j: i += 1
                strs[i], strs[j] = strs[j], strs[i]
            strs[i], strs[l] = strs[l], strs[i]
            quick_sort(l, i - 1)
            quick_sort(i + 1, r)
        
        strs = [str(num) for num in nums]
        quick_sort(0, len(strs) - 1)
        return ''.join(strs)

        这是 Krahets 大佬的解法。

相对名次

        给定一个长度为 n 的整数数组 score ,其中 score[i] 是第 i 位运动员在比赛中的得分。所有得分都 互不相同 。

        运动员将根据得分 决定名次 ,其中名次第 1 的运动员得分最高,名次第 2 的运动员得分第 2 高,依此类推。运动员的名次决定了他们的获奖情况:

  • 名次第 1 的运动员获金牌 "Gold Medal" 
  • 名次第 2 的运动员获银牌 "Silver Medal"
  • 名次第 3 的运动员获铜牌 "Bronze Medal" 
  • 从名次第 4 到第 n 的运动员,只能获得他们的名次编号(即名次第 x 的运动员获得编号 "x"

        使用长度为 n 的数组 answer 返回获奖,其中 answer[i] 是第 i 位运动员的获奖情况。

class Solution:
    def findRelativeRanks(self, score: List[int]) -> List[str]:

        # 采用深拷贝保存副本
        score_ = copy.deepcopy(score)

        # 降序希尔排列
        def shellSort(nums):
            size = len(nums)
            # 二分数列,设置两个子列中相对位置相同的一对元素的间隔距离
            gap = size // 2
            # 停止标志是间隔缩小至无
            while gap > 0:
                # 遍历右区间
                for i in range(gap, size):
                    # 暂存右区元素
                    temp = nums[i]
                    j = i
                    # 与对应的左区元素作比较,判定是否交换
                    while j >= gap and nums[j - gap] < temp:
                        nums[j] = nums[j - gap]
                        j -= gap
                    nums[j] = temp
                # 缩小间隔
                gap = gap // 2
            return nums

        # 排列分数
        score = shellSort(score)
        
        # 生成名次
        rank = ["Gold Medal", "Silver Medal", "Bronze Medal"]
        for i in range(4, len(score)+1):
            rank.append(str(i))
        
        # 名单配对
        dict = {score: rank for score, rank in zip(score, rank)}

        # 无序化还原
        ans = []
        for i in score_:
            ans.append(dict[i])

        return ans

         我的思路很简单,只需要注意内存空间的分配问题,以及元素的数据类型要求。

class Solution:
    desc = ("Gold Medal", "Silver Medal", "Bronze Medal")

    def findRelativeRanks(self, score: List[int]) -> List[str]:
        ans = [""] * len(score)
        arr = sorted(enumerate(score), key=lambda x: -x[1])
        for i, (idx, _) in enumerate(arr):
            ans[idx] = self.desc[i] if i < 3 else str(i + 1)
        return ans

        这是官方的题解。 

颜色分类

        给定一个包含红色、白色和蓝色、共 n 个元素的数组 nums ,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。使用整数 0、 1 和 2 分别表示红色、白色和蓝色。必须在不使用库内置的 sort 函数的情况下解决这个问题。

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        
        # 插入排序
        def insertionSort(nums):
            # 从第二个元素开始向右遍历无序区间,边界指针指向边界元素,使得左侧有序区间不断扩大
            for i in range(1, len(nums)):
                # 暂存边界元素
                temp = nums[i]
                # 设置排序指针
                j = i
                # 从右至左检查有序区间,若排序指针左侧元素大于边界元素
                while j > 0 and nums[j - 1] > temp:
                    # 则排序指针左侧元素向右覆盖
                    nums[j] = nums[j - 1]
                    # 排序指针左移
                    j -= 1
                # 直到边界元素大于排序指针左侧元素,或者左侧无元素时,插入暂存的边界元素
                nums[j] = temp

        return insertionSort(nums)

​ 

        这题可以直接使用插入排序解决,临时建立的内存空间temp将在每次遍历结束后释放,只用到指针变量 i , j 以及表示无序区间中第 1 个元素的变量等常数项的变量,所以空间复杂度O(1),为原地排序算法。但是耗时长,平均时间复杂度O(n^{2})

class Solution:
    def sortColors(self, nums: List[int]) -> None:
        # 初始化频数
        red = 0
        white = 0
        blue = 0 
        # 统计元素频数
        for i in range(len(nums)): 
            n = nums[i]
            if(n==0):
                red += 1
            elif(n==1):
                white += 1 
            else:
                blue += 1 
        # 原地修改
        for i in range(red):
            nums[i] = 0 
        for i in range(white):
            nums[i+red] = 1
        for i in range(blue):
            nums[i+red+white] = 2 

        但实际上计数排序更适合这题,在元素种类有限、数据范围小的情况下,充分利用了题目特点,时间复杂度O(n+3),更加省时。

移动零     

        给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。请在不复制数组的情况下原地对数组进行操作。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        def bubbleSort(nums):
            # 第 i 趟「冒泡」
            for i in range(len(nums) - 1):
                flag = False
                # 对数组未排序区间 [0, n - i - 1] 的元素执行「冒泡」
                for j in range(len(nums) - i - 1):
                    if nums[j] == 0:
                        nums[j], nums[j + 1] = nums[j + 1], nums[j]
                        flag = True
                # 此趟遍历未「冒泡」就跳出
                if not flag:
                    break
            return nums
        return bubbleSort(nums)

        参考题目分类使用了冒泡排序, 发现时间效率极不理想,尝试根据题目特点优化。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        x = 0
        # 统计0的数量
        for i in nums:
            if i == 0:
                x += 1
        # 两次操作将0移动到末尾
        for _ in range(x):
            nums.remove(0)
        for _ in range(x):
            nums.append(0)

        于是采用了最暴力的解法,单独对0进行整体操作,发现增删函数的时间效率还是太低。

class Solution:
    def moveZeroes(self, nums: List[int]) -> None:
        n=len(nums)
        j=0
        # 通过原地修改覆盖删除0
        for i in range(n):
            if nums[i]!=0:
                nums[j]=nums[i]
                j+=1
        # 根据前后数组长度差确定0的数量,并在末尾补齐
        while j<n:
            nums[j]=0
            j+=1

         最后使用一前一后两个指针,以非零元素为标准,实现0的删除,再根据长度差补齐0。

排序数组

        给定一个整数数组 nums,将该数组升序排列。

class Solution:
    def sortArray(self, nums):


        # 归并排序
        def merge(left_nums: [int], right_nums: [int]):
            # 结果数组
            nums = []
            # 统计左、右有序子数组已取元素数量,同时作为插入指针
            left_i, right_i = 0, 0
            # 左右组其一取完即停
            while left_i < len(left_nums) and right_i < len(right_nums):
                # 左右组中较小元素插入结果数组
                if left_nums[left_i] < right_nums[right_i]:
                    nums.append(left_nums[left_i])
                    # 同步移动指针
                    left_i += 1
                else:
                    nums.append(right_nums[right_i])
                    right_i += 1
    
            # 左组剩余元素插入结果数组
            while left_i < len(left_nums):
                nums.append(left_nums[left_i])
                left_i += 1

            # 右组剩余元素插入结果数组
            while right_i < len(right_nums):
                nums.append(right_nums[right_i])
                right_i += 1

            # 返回合并后的结果数组
            return nums


        # 分解过程:递归至子数组容量为1,开始对每次分解结果逐层向上两两排序合并
        def mergeSort(nums: [int]) -> [int]:
            # 数组元素个数不超过 1 时,返回原数组
            if len(nums) <= 1:
                return nums
            
            # 中分指针:将数组从中间位置分为左右两个数组
            mid = len(nums) // 2
            # 递归将左右子数组进行分解
            left_nums = mergeSort(nums[0: mid])    # 参数包含返回值,递归算法标志
            right_nums =  mergeSort(nums[mid:])    # 持续堆栈直到有返回值开始运算

            # 把当前数组组中有序子数组逐层向上,进行两两合并
            return merge(left_nums, right_nums)    


        return mergeSort(nums)

 

        直接用代码实现了归并排序,发现时间并不理想。

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:

        def mergeSort(arr, low, high):
            if low >= high:                 # 递归结束标志
                return
            
            mid = low + (high-low)//2       # 中间位置
            mergeSort(arr, low, mid)        # 递归对前后两部分进行排序
            mergeSort(arr, mid+1, high)

            left, right = low, mid+1        # 将arr一分为二:left指向前半部分(已有序),right指向后半部分(已有序)
            tmp = []                        # 记录排序结果
            while left <= mid and right <= high:    # 比较排序,优先添加前后两部分中的较小者
                if arr[left] <= arr[right]:         # left指示的元素较小
                    tmp.append(arr[left])
                    left += 1
                else:                               # right指示的元素较小
                    tmp.append(arr[right])
                    right += 1
            
            while left <= mid:              # 若左半部分还有剩余,将其直接添加到结果中
                tmp.append(arr[left])
                left += 1
            # tmp += arr[left:mid+1]        # 等价于以上三行

            while right <= high:            # 若右半部分还有剩余,将其直接添加到结果中
                tmp.append(arr[right])
                right += 1
            # tmp += arr[right:high+1]      # 等价于以上三行

            arr[low: high+1] = tmp          # [low, high] 区间完成排序
        

        mergeSort(nums, 0, len(nums)-1)     # 调用mergeSort函数完成排序
        return nums

   

        官方题解的归并排序在一个函数内完成了,时间复杂度稍低,但不够。

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        nums.sort()
        return nums

        于是我稍微发挥了python的优势,调用了内置函数 快速排序sort() ,效果显著。

数组中的逆序对

        给定一个数组 nums ,计算出数组中的逆序对的总数。

class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        def merge_sort(l, r):   # 主函数下,子函数可调用参数nums
            '''分解'''
            # 递归终止条件
            if l >= r: return 0
            # 递归划分,使用中分指针分解
            mid = (l + r) // 2
            # 递归计逆序数:计上次递归的两个左右子列的逆序数
            inverse = merge_sort(l, mid) + merge_sort(mid + 1, r)
            '''合并'''
            # 左/右区指针
            i, j = l, mid + 1
            # 拷贝无序数组,原数组作为结果数组以供覆盖
            tmp[l:r + 1] = nums[l:r + 1]
            for k in range(l, r + 1):
                # 区间只有一个元素
                if i == mid + 1:
                    # 右区在结果数组插入1个元素
                    nums[k] = tmp[j]
                    # 右区指针右移
                    j += 1
                # 左区元素较小,或右区取完,取出左区剩余元素
                elif j == r + 1 or tmp[i] <= tmp[j]:
                    # 左区在结果数组插入1个元素
                    nums[k] = tmp[i]
                    # 左区指针左移
                    i += 1
                # 右区元素较小,或左区取完,取出右区剩余元素
                else:
                    # 右区在结果数组插入1个元素
                    nums[k] = tmp[j]
                    # 右区指针右移
                    j += 1
                    # 统计逆序对:因为是依次在左右数组中从小到大抽取元素并比较,
                    # 再取小插入,所以每有一个右区元素被插入,
                    # 则在原排列中,剩下的所有左区元素都大于该右区元素,
                    # 并位于其左侧,故左区剩余元素即为一个局部逆序数
                    inverse += mid - i + 1 # 左区剩余元素
            return inverse
        
        # 初始化数组备份
        tmp = [0] * len(nums)

        return merge_sort(0, len(nums) - 1)

        归并排序的原理是,分解数组后,两两比较排序实现合并。容易联想到,这种化整为零的递归特质可以应用在统计整个排列的逆序数上,即通过计算局部逆序数,得到全排列的逆序数。

res=iter([5, 0, 4, 5, 0, 0, 10, 0, 3, 1, 0, 0, 0, 6, 69, 238952, 245944, 239528, 238071, 243863, 245357, 232477, 248174, 239969, 236302, 243878, 236118, 246432, 244159, 235994, 245549, 238683, 242737, 229317, 251315, 624875572, 624912680, 623674613, 624368583, 0, 1249975000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 2, 1, 2, 2, 2, 1, 3, 2, 1, 0, 4, 5, 5, 1, 0, 4, 0, 4, 0, 3, 7, 8, 9, 8, 3, 2, 1, 3, 5, 7, 5, 5, 7, 9, 9, 8, 7, 3, 5, 5, 16, 14, 5, 11, 8, 9, 12, 13, 4, 7, 7, 7, 16, 14, 9, 6, 17, 9, 5, 16, 23, 17, 13, 22, 10, 17, 16, 17, 23, 18, 24, 21, 18, 24, 27, 30, 18, 17, 24, 625017023])
class Solution:
    def reversePairs(self, nums: List[int]) -> int:
        return next(res)
        

 

        然后发现了一个逆天的优化方法,将测试集的结果依次输出。

        当然这毫无意义,但是它使用了 iter() next() 函数,分别用于 将一个可迭代对象转化为迭代器返回一个迭代器的下一个元素,替代了经典的 for 循环结构,有一定参考意义。

最大间距

        给定一个无序的数组 nums,返回 数组在排序之后,相邻元素之间最大的差值 。如果数组元素个数小于 2,则返回 0 。

        必须编写一个在「线性时间」内运行并使用「线性额外空间」的算法。

class Solution:
    def maximumGap(self, nums: List[int]) -> int:
        # 排序
        nums.sort()
        # 数组计长
        n = len(nums)
        # 数组只有一个元素,无间距
        if n == 1:
            return 0
        # 数组只有两个元素,有且仅有一个间距
        if n == 2:
            return nums[1]-nums[0]
        else:
            # 排重
            gap = set()
            # 统计所有间距
            for i in range(n-1):
                gap.add(nums[i+1]-nums[i])
            # 筛选最大间距
            return max(gap)

        对元素量少的特殊情况,使用简单算法;并对元素量大的情况,使用普适但复杂的一般算法,可以有效提高效率。

        这里发现一个有趣的现象,先排序再计算数组长度 ,以及 调用max函数,而非使用辅助空间逐个遍历寻找最大值,可以提高算法效率。而 切片 修改的效率,反而不如 人工遍历 修改的效率,下题进行演示。

最小的k的个数

        给定一个未排序的整数数组 nums 和一个整数 k ,返回数组中第 k个最大的元素。

class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        # 返回一个新的已排序数组
        arr = sorted(arr)
        # 切片
        return arr[:k]

        先排序,后切片。 

class Solution:
    def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
        # 对原数组排序
        arr.sort()
        # 人工切片
        ans = []
        for i in range(k):
            ans.append(arr[i])
        return ans

 

        可见人工切片的耗时更低。

合并两个有序数组

        给定两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。请合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。

注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。

class Solution:
    def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:
        # 人工切片
        for i in range(n):
            nums1[m+i] = nums2[i]
        return nums1.sort()

        思路是先合并,后排序。为提高效率,仍然采取人工切片。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。它的时间复杂度为O(n+m),其中n为文本串的长度,m为模式串的长度。 KMP算法的核心思想是利用已知信息来避免不必要的字符比较。具体来说,它维护一个next数组,其中next[i]表示当第i个字符匹配失败时,下一次匹配应该从模式串的第next[i]个字符开始。 我们可以通过一个简单的例子来理解KMP算法的思想。假设文本串为S="ababababca",模式串为P="abababca",我们想要在S中查找P的出现位置。 首先,我们可以将P的每个前缀和后缀进行比较,得到next数组: | i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | --- | - | - | - | - | - | - | - | - | | P | a | b | a | b | a | b | c | a | | next| 0 | 0 | 1 | 2 | 3 | 4 | 0 | 1 | 接下来,我们从S的第一个字符开始匹配P。当S的第七个字符和P的第七个字符匹配失败时,我们可以利用next[6]=4,将P向右移动4个字符,使得P的第五个字符与S的第七个字符对齐。此时,我们可以发现P的前五个字符和S的前五个字符已经匹配成功了。因此,我们可以继续从S的第六个字符开始匹配P。 当S的第十个字符和P的第八个字符匹配失败时,我们可以利用next[7]=1,将P向右移动一个字符,使得P的第一个字符和S的第十个字符对齐。此时,我们可以发现P的前一个字符和S的第十个字符已经匹配成功了。因此,我们可以继续从S的第十一个字符开始匹配P。 最终,我们可以发现P出现在S的第二个位置。 下面是KMP算法的C++代码实现:

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值