Leetcode 01-算法入门与数组-③数组排序

LeetCode 01-算法入门与数组-③数组排序

一. 冒泡排序

1. 冒泡排序算法思想

冒泡排序(Bubble Sort)基本思想

经过多次迭代,通过相邻元素之间的比较与交换,使值较小的元素逐步从后面移到前面,值较大的元素从前面移到后面。

这个过程就像水底的气泡一样从底部向上「冒泡」到水面,这也是冒泡排序法名字的由来。

接下来,我们使用「冒泡」的方式来模拟一下这个过程。

  1. 首先将数组想象是一排「泡泡」,元素值的大小与泡泡的大小成正比。
  2. 然后从左到右依次比较相邻的两个「泡泡」:
    1. 如果左侧泡泡大于右侧泡泡,则交换两个泡泡的位置。
    2. 如果左侧泡泡小于等于右侧泡泡,则两个泡泡保持不变。
  3. 1 1 1 趟遍历完成之后,最大的泡泡就会放置到所有泡泡的最右侧,就像是「泡泡」从水底向上浮到了水面。

<1>

在这里插入图片描述

<2>

在这里插入图片描述

<3>

在这里插入图片描述

<4>

在这里插入图片描述

<5>

在这里插入图片描述

<6>

在这里插入图片描述

<7>

在这里插入图片描述

2. 冒泡排序算法步骤

假设数组的元素个数为 n n n 个,则冒泡排序的算法步骤如下:

  1. 1 1 1 趟「冒泡」:对前 n n n 个元素执行「冒泡」,从而使第 1 1 1 个值最大的元素放置在正确位置上。
    1. 先将序列中第 1 1 1 个元素与第 2 2 2 个元素进行比较,如果前者大于后者,则两者交换位置,否则不交换。
    2. 然后将第 2 2 2 个元素与第 3 3 3 个元素比较,如果前者大于后者,则两者交换位置,否则不交换。
    3. 依次类推,直到第 n − 1 n - 1 n1 个元素与第 n n n 个元素比较(或交换)为止。
    4. 经过第 1 1 1 趟排序,使得 n n n 个元素中第 i i i 个值最大元素被安置在第 n n n 个位置上。
  2. 2 2 2 趟「冒泡」:对前 n − 1 n - 1 n1 个元素执行「冒泡」,从而使第 2 2 2 个值最大的元素放置在正确位置上。
    1. 先将序列中第 1 1 1 个元素与第 2 2 2 个元素进行比较,若前者大于后者,则两者交换位置,否则不交换。
    2. 然后将第 2 2 2 个元素与第 3 3 3 个元素比较,若前者大于后者,则两者交换位置,否则不交换。
    3. 依次类推,直到对 n − 2 n - 2 n2 个元素与第 n − 1 n - 1 n1 个元素比较(或交换)为止。但是少时诵诗书所所所所是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒是撒
    4. 经过第 2 2 2 趟排序,使得数组中第 2 2 2 个值最大元素被安置在第 n n n 个位置上。
  3. 依次类推,重复上述「冒泡」过程,直到某一趟排序过程中不出现元素交换位置的动作,则排序结束。

我们以 [ 5 , 2 , 3 , 6 , 1 , 4 ] [5, 2, 3, 6, 1, 4] [5,2,3,6,1,4] 为例,演示一下冒泡排序的整个过程。

在这里插入图片描述

3. 冒泡排序代码实现

class Solution:def bubbleSort(self, nums: [int]) -> [int]:# 第 i 趟「冒泡」for i in range(len(nums) - 1):flag = False    # 是否发生交换的标志位# 从数组中前 n - i + 1 个元素的第 1 个元素开始,相邻两个元素进行比较for j in range(len(nums) - i - 1):# 相邻两个元素进行比较,如果前者大于后者,则交换位置if nums[j] > nums[j + 1]:nums[j], nums[j + 1] = nums[j + 1], nums[j]flag = Trueif not flag:    # 此趟遍历未交换任何元素,直接跳出breakreturn numsdef sortArray(self, nums: [int]) -> [int]:return self.bubbleSort(nums)

4. 冒泡排序算法分析

  • 最佳时间复杂度 O ( n ) O(n) O(n)。最好的情况下(初始时序列已经是升序排列),只需经过 1 1 1 趟排序,总共经过 n n n 次元素之间的比较,并且不移动元素,算法就可以结束排序。因此,冒泡排序算法的最佳时间复杂度为 O ( n ) O(n) O(n)
  • 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)。最差的情况下(初始时序列已经是降序排列,或者最小值元素处在序列的最后),则需要进行 n n n 趟排序,总共进行 ∑ i = 2 n ( i − 1 ) = n ( n − 1 ) 2 ∑^n_{i=2}(i−1) = \frac{n(n−1)}{2} i=2n(i1)=2n(n1) 次元素之间的比较,因此,冒泡排序算法的最坏时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度 O ( 1 ) O(1) O(1)。冒泡排序为原地排序算法,只用到指针变量 i i i j j j 以及标志位 f l a g flag flag 等常数项的变量。
  • 冒泡排序适用情况:冒泡排序方法在排序过程中需要移动较多次数的元素,并且排序时间效率比较低。因此,冒泡排序方法比较适合于参加排序序列的数据量较小的情况,尤其是当序列的初始状态为基本有序的情况。
  • 排序稳定性:由于元素交换是在相邻元素之间进行的,不会改变相等元素的相对顺序,因此,冒泡排序法是一种 稳定排序算法

二. 选择排序

1. 选择排序算法思想

选择排序(Selection Sort)基本思想

将数组分为两个区间:左侧为已排序区间,右侧为未排序区间。每趟从未排序区间中选择一个值最小的元素,放到已排序区间的末尾,从而将该元素划分到已排序区间。

选择排序是一种简单直观的排序算法,其思想简单,代码也相对容易。

2. 选择排序算法步骤

假设数组的元素个数为 n n n 个,则选择排序的算法步骤如下:

  1. 初始状态下,无已排序区间,未排序区间为 [ 0 , n − 1 ] [0, n - 1] [0,n1]
  2. 1 1 1 趟选择:
    1. 遍历未排序区间 [ 0 , n − 1 ] [0, n - 1] [0,n1],使用变量 m i n ‾ i min\underline{}i mini 记录区间中值最小的元素位置。
    2. m i n ‾ i min\underline{}i mini 与下标为 0 0 0 处的元素交换位置。如果下标为 0 0 0 处元素就是值最小的元素位置,则不用交换。
    3. 此时, [ 0 , 0 ] [0, 0] [0,0] 为已排序区间, [ 1 , n − 1 ] [1, n - 1] [1,n1](总共 n − 1 n - 1 n1 个元素)为未排序区间。
  3. 2 2 2 趟选择:
    1. 遍历未排序区间 [ 1 , n − 1 ] [1, n - 1] [1,n1],使用变量 m i n ‾ i min\underline{}i mini 记录区间中值最小的元素位置。
    2. m i n ‾ i min\underline{}i mini 与下标为 1 1 1 处的元素交换位置。如果下标为 1 1 1 处元素就是值最小的元素位置,则不用交换。
    3. 此时, [ 0 , 1 ] [0, 1] [0,1] 为已排序区间, [ 2 , n − 1 ] [2, n - 1] [2,n1](总共 n − 2 n - 2 n2 个元素)为未排序区间。
  4. 依次类推,对剩余未排序区间重复上述选择过程,直到所有元素都划分到已排序区间,排序结束。

我们以 [ 5 , 2 , 3 , 6 , 1 , 4 ] [5, 2, 3, 6, 1, 4] [5,2,3,6,1,4] 为例,演示一下选择排序的整个过程。

<1>

在这里插入图片描述

<2>

在这里插入图片描述

<3>
在这里插入图片描述

<4>

在这里插入图片描述

<5>

在这里插入图片描述

<6>

在这里插入图片描述

<7>

在这里插入图片描述

3. 选择排序代码实现

class Solution:def selectionSort(self, nums: [int]) -> [int]:for i in range(len(nums) - 1):# 记录未排序区间中最小值的位置min_i = ifor j in range(i + 1, len(nums)):if nums[j] < nums[min_i]:min_i = j# 如果找到最小值的位置,将 i 位置上元素与最小值位置上的元素进行交换if i != min_i:nums[i], nums[min_i] = nums[min_i], nums[i]return numsdef sortArray(self, nums: [int]) -> [int]:return self.selectionSort(nums)

4. 选择排序算法分析

  • 时间复杂度 O ( n 2 ) O(n^2) O(n2)。排序法所进行的元素之间的比较次数与序列的原始状态无关,时间复杂度总是 O ( n 2 ) O(n^2) O(n2)
    • 这是因为无论序列中元素的初始排列状态如何,第 i i i 趟排序要找出值最小元素都需要进行 n − i n − i ni 次元素之间的比较。因此,整个排序过程需要进行的元素之间的比较次数都相同,为 ∑ i = 2 n ( i − 1 ) = n ( n − 1 ) 2 ∑^n_{i=2}(i - 1) = \frac{n(n−1)}{2} i=2n(i1)=2n(n1) 次。
  • 空间复杂度 O ( 1 ) O(1) O(1)。选择排序算法为原地排序算法,只用到指针变量 i i i j j j 以及最小值位置 m i n ‾ i min\underline{}i mini 等常数项的变量。
  • 选择排序适用情况:选择排序方法在排序过程中需要移动较多次数的元素,并且排序时间效率比较低。因此,选择排序方法比较适合于参加排序序列的数据量较小的情况。选择排序的主要优点是仅需要原地操作无需占用其他空间就可以完成排序,因此在空间复杂度要求较高时,可以考虑选择排序。
  • 排序稳定性:由于值最小元素与未排序区间第 1 1 1 个元素的交换动作是在不相邻的元素之间进行的,因此很有可能会改变相等元素的相对顺序,因此,选择排序法是一种 不稳定排序算法

三. 插入排序

1. 插入排序算法思想

插入排序(Insertion Sort)基本思想

将数组分为两个区间:左侧为有序区间,右侧为无序区间。每趟从无序区间取出一个元素,然后将其插入到有序区间的适当位置。

插入排序在每次插入一个元素时,该元素会在有序区间找到合适的位置,因此每次插入后,有序区间都会保持有序。

2. 插入排序算法步骤

假设数组的元素个数为 n n n 个,则插入排序的算法步骤如下:

  1. 初始状态下,有序区间为 [ 0 , 0 ] [0, 0] [0,0],无序区间为 [ 1 , n − 1 ] [1, n - 1] [1,n1]
  2. 1 1 1 趟插入:
    1. 取出无序区间 [ 1 , n − 1 ] [1, n - 1] [1,n1] 中的第 1 1 1 个元素,即 n u m s [ 1 ] nums[1] nums[1]
    2. 从右到左遍历有序区间中的元素,将比 n u m s [ 1 ] nums[1] nums[1] 小的元素向后移动 1 1 1 位。
    3. 如果遇到大于或等于 n u m s [ 1 ] nums[1] nums[1] 的元素时,说明找到了插入位置,将 n u m s [ 1 ] nums[1] nums[1] 插入到该位置。
    4. 插入元素后有序区间变为 [ 0 , 1 ] [0, 1] [0,1],无序区间变为 [ 2 , n − 1 ] [2, n - 1] [2,n1]
  3. 2 2 2 趟插入:
    1. 取出无序区间 [ 2 , n − 1 ] [2, n - 1] [2,n1] 中的第 1 1 1 个元素,即 n u m s [ 2 ] nums[2] nums[2]
    2. 从右到左遍历有序区间中的元素,将比 n u m s [ 2 ] nums[2] nums[2] 小的元素向后移动 1 1 1 位。
    3. 如果遇到大于或等于 n u m s [ 2 ] nums[2] nums[2] 的元素时,说明找到了插入位置,将 n u m s [ 2 ] nums[2] nums[2] 插入到该位置。
    4. 插入元素后有序区间变为 [ 0 , 2 ] [0, 2] [0,2],无序区间变为 [ 3 , n − 1 ] [3, n - 1] [3,n1]
  4. 依次类推,对剩余无序区间中的元素重复上述插入过程,直到所有元素都插入到有序区间中,排序结束。

我们以 [ 5 , 2 , 3 , 6 , 1 , 4 ] [5, 2, 3, 6, 1, 4] [5,2,3,6,1,4] 为例,演示一下插入排序的整个过程。

在这里插入图片描述

3. 插入排序代码实现

class Solution:def insertionSort(self, nums: [int]) -> [int]:# 遍历无序区间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] = tempreturn numsdef sortArray(self, nums: [int]) -> [int]:return self.insertionSort(nums)

4. 插入排序算法分析

  • 最佳时间复杂度 O ( n ) O(n) O(n)。最好的情况下(初始时区间已经是升序排列),每个元素只进行一次元素之间的比较,因而总的比较次数最少,为 ∑ i = 2 n 1 = n − 1 ∑^n_{i = 2}1 = n − 1 i=2n1=n1,并不需要移动元素(记录),这是最好的情况。
  • 最差时间复杂度 O ( n 2 ) O(n^2) O(n2)。最差的情况下(初始时区间已经是降序排列),每个元素 n u m s [ i ] nums[i] nums[i] 都要进行 i − 1 i - 1 i1 次元素之间的比较,元素之间总的比较次数达到最大值,为 ∑ i = 2 n ( i − 1 ) = n ( n − 1 ) 2 ∑^n_{i=2}(i − 1) = \frac{n(n−1)}{2} i=2n(i1)=2n(n1)
  • 平均时间复杂度 O ( n 2 ) O(n^2) O(n2)。如果区间的初始情况是随机的,即参加排序的区间中元素可能出现的各种排列的概率相同,则可取上述最小值和最大值的平均值作为插入排序时所进行的元素之间的比较次数,约为 n 2 4 \frac{n^2}{4} 4n2。由此得知,插入排序算法的平均时间复杂度为 O ( n 2 ) O(n^2) O(n2)
  • 空间复杂度 O ( 1 ) O(1) O(1)。插入排序算法为原地排序算法,只用到指针变量 i i i j j j 以及表示无序区间中第 1 1 1 个元素的变量等常数项的变量。
  • 排序稳定性:在插入操作过程中,每次都讲元素插入到相等元素的右侧,并不会改变相等元素的相对顺序。因此,插入排序方法是一种 稳定排序算法

四. 练习题目1

1. 剑指 Offer 45. 把数组排成最小的数

1.1 题目大意

描述:给定一个非负整数数组 nums

要求:将数组中的数字拼接起来排成一个数,打印能拼接出的所有数字中的最小的一个。

说明

  • 0 < n u m s . l e n g t h ≤ 100 0 < nums.length \le 100 0<nums.length100
  • 输出结果可能非常大,所以你需要返回一个字符串而不是整数。
  • 拼接起来的数字可能会有前导 0,最后结果不需要去掉前导 0

示例

输入:[3,30,34,5,9]
输出:"3033459"

1.2 解题思路

思路 1:自定义排序

本质上是给数组进行排序。假设 xy 是数组 nums 中的两个元素。则排序的判断规则如下所示:

  • 如果拼接字符串 x + y > y + x,则 x 大于 y y 应该排在 x 前面,从而使拼接起来的数字尽可能的小。
  • 反之,如果拼接字符串 x + y < y + x,则 x 小于 y x 应该排在 y 前面,从而使拼接起来的数字尽可能的小。

按照上述规则,对原数组进行排序。这里使用了 functools.cmp_to_key 自定义排序函数。

思路 1:自定义排序代码
from functools import cmp_to_keyclass Solution:def minNumber(self, nums: List[int]) -> str:nums = [*map(str, nums)]nums.sort(key=cmp_to_key(lambda x, y: - (x + y < y + x)))return "".join(nums)
思路 1:复杂度分析
  • 时间复杂度 O ( n × log ⁡ 2 n ) O(n \times \log_2n) O(n×log2n)。排序算法的时间复杂度为 O ( n × log ⁡ 2 n ) O(n \times \log_2n) O(n×log2n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

2. 0283. 移动零

2.1 题目大意

描述:给定一个数组 nums

要求:将所有 0 移动到末尾,并保持原有的非 0 数字的相对顺序。

说明

  • 只能在原数组上进行操作。
  • 1 ≤ n u m s . l e n g t h ≤ 1 0 4 1 \le nums.length \le 10^4 1nums.length104
  • − 2 31 ≤ n u m s [ i ] ≤ 2 31 − 1 -2^{31} \le nums[i] \le 2^{31} - 1 231nums[i]2311

示例

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]输入: nums = [0]
输出: [0]

2.2 解题思路

思路 1:快慢指针
  1. 使用两个指针 slowfastslow 指向处理好的非 0 数字数组的尾部,fast 指针指向当前待处理元素。
  2. 不断向右移动 fast 指针,每次移动到非零数,则将左右指针对应的数交换,交换同时将 slow 右移。
  3. 此时,slow 指针左侧均为处理好的非零数,而从 slow 指针指向的位置开始, fast 指针左边为止都为 0

遍历结束之后,则所有 0 都移动到了右侧,且保持了非零数的相对位置。

思路 1:代码
class Solution:def moveZeroes(self, nums: List[int]) -> None:s = 0for f in range(len(nums)):if nums[f]:if f - s:nums[s] = nums[f]nums[f] = 0s += 1
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

3. 0912. 排序数组

3.1 题目大意

描述:给定一个整数数组 nums

要求:将该数组升序排列。

说明

  • 1 ≤ n u m s . l e n g t h ≤ 5 ∗ 1 0 4 1 \le nums.length \le 5 * 10^4 1nums.length5104
  • − 5 ∗ 1 0 4 ≤ n u m s [ i ] ≤ 5 ∗ 1 0 4 -5 * 10^4 \le nums[i] \le 5 * 10^4 5104nums[i]5104

示例

输入:nums = [5,2,3,1]
输出:[1,2,3,5]输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]

3.2 解题思路

思路 1:真 · 快速排序

真 · 快速排序基本思想

  • 调用API
思路 1:代码
class Solution:def sortArray(self, nums: List[int]) -> List[int]:nums.sort()return nums
思路 1:复杂度分析
  • 时间复杂度 O ( n × log ⁡ 2 n ) O(n \times \log_2 n) O(n×log2n)
  • 空间复杂度 O ( n ) O(n) O(n)

五. 归并排序

1. 归并排序算法思想

归并排序(Merge Sort)基本思想

采用经典的分治策略,先递归地将当前数组平均分成两半,然后将有序数组两两合并,最终合并成一个有序数组。

2. 归并排序算法步骤

假设数组的元素个数为 n n n 个,则归并排序的算法步骤如下:

  1. 分解过程:先递归地将当前数组平均分成两半,直到子数组长度为 1 1 1
    1. 找到数组中心位置 m i d mid mid,从中心位置将数组分成左右两个子数组 l e f t ‾ n u m s left\underline{}nums leftnums r i g h t ‾ n u m s right\underline{}nums rightnums
    2. 对左右两个子数组 l e f t ‾ n u m s left\underline{}nums leftnums r i g h t ‾ n u m s right\underline{}nums rightnums 分别进行递归分解。
    3. 最终将数组分解为 n n n 个长度均为 1 1 1 的有序子数组。
  2. 归并过程:从长度为 1 1 1 的有序子数组开始,依次将有序数组两两合并,直到合并成一个长度为 n n n 的有序数组。
    1. 使用数组变量 n u m s nums nums 存放合并后的有序数组。
    2. 使用两个指针 l e f t ‾ i left\underline{}i lefti r i g h t ‾ i right\underline{}i righti 分别指向两个有序子数组 l e f t ‾ n u m s left\underline{}nums leftnums r i g h t ‾ n u m s right\underline{}nums rightnums 的开始位置。
    3. 比较两个指针指向的元素,将两个有序子数组中较小元素依次存入到结果数组 n u m s nums nums 中,并将指针移动到下一位置。
    4. 重复步骤 3 3 3,直到某一指针到达子数组末尾。
    5. 将另一个子数组中的剩余元素存入到结果数组 n u m s nums nums 中。
    6. 返回合并后的有序数组 n u m s nums nums

我们以 [ 0 , 5 , 7 , 3 , 1 , 6 , 8 , 4 ] [0, 5, 7, 3, 1, 6, 8, 4] [0,5,7,3,1,6,8,4] 为例,演示一下归并排序的整个过程。

在这里插入图片描述

3. 归并排序代码实现

class Solution:# 合并过程def merge(self, left_nums: [int], right_nums: [int]):nums = []left_i, right_i = 0, 0while 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 += 1else: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# 分解过程def mergeSort(self, nums: [int]) -> [int]:# 数组元素个数小于等于 1 时,直接返回原数组if len(nums) <= 1:return numsmid = len(nums) // 2                        # 将数组从中间位置分为左右两个数组left_nums = self.mergeSort(nums[0: mid])    # 递归将左子数组进行分解和排序right_nums =  self.mergeSort(nums[mid:])    # 递归将右子数组进行分解和排序return self.merge(left_nums, right_nums)    # 把当前数组组中有序子数组逐层向上,进行两两合并def sortArray(self, nums: [int]) -> [int]:return self.mergeSort(nums)

4. 归并排序算法分析

  • 时间复杂度 O ( n × log ⁡ n ) O(n \times \log n) O(n×logn)。归并排序算法的时间复杂度等于归并趟数与每一趟归并的时间复杂度乘积。子算法 merge(left_nums, right_nums): 的时间复杂度是 O ( n ) O(n) O(n),因此,归并排序算法总的时间复杂度为 O ( n × log ⁡ n ) O(n \times \log n) O(n×logn)
  • 空间复杂度 O ( n ) O(n) O(n)。归并排序方法需要用到与参加排序的数组同样大小的辅助空间。因此,算法的空间复杂度为 O ( n ) O(n) O(n)
  • 排序稳定性:因为在两个有序子数组的归并过程中,如果两个有序数组中出现相等元素,merge(left_nums, right_nums): 算法能够使前一个数组中那个相等元素先被复制,从而确保这两个元素的相对顺序不发生改变。因此,归并排序算法是一种 稳定排序算法

六. 希尔排序

1. 希尔排序算法思想

希尔排序(Shell Sort)基本思想

将整个数组切按照一定的间隔取值划分为若干个子数组,每个子数组分别进行插入排序。然后逐渐缩小间隔进行下一轮划分子数组和对子数组进行插入排序。直至最后一轮排序间隔为 1 1 1,对整个数组进行插入排序。

2. 希尔排序算法步骤

假设数组的元素个数为 n n n 个,则希尔排序的算法步骤如下:

  1. 确定一个元素间隔数 g a p gap gap
  2. 将参加排序的数组按此间隔数从第 1 1 1 个元素开始一次分成若干个子数组,即分别将所有位置相隔为 g a p gap gap 的元素视为一个子数组。
  3. 在各个子数组中采用某种排序算法(例如插入排序算法)进行排序。
  4. 减少间隔数,并重新将整个数组按新的间隔数分成若干个子数组,再分别对各个子数组进行排序。
  5. 依次类推,直到间隔数 g a p gap gap 值为 1 1 1,最后进行一次排序,排序结束。

我们以 [ 7 , 2 , 6 , 8 , 0 , 4 , 1 , 5 , 9 , 3 ] [7, 2, 6, 8, 0, 4, 1, 5, 9, 3] [7,2,6,8,0,4,1,5,9,3] 为例,演示一下希尔排序的整个过程。

<1>

在这里插入图片描述

<2>

在这里插入图片描述

<3>

在这里插入图片描述

<4>

在这里插入图片描述

<5>

在这里插入图片描述

<6>

在这里插入图片描述

<7>

在这里插入图片描述

3. 希尔排序代码实现

class Solution:def shellSort(self, nums: [int]) -> [int]:size = len(nums)gap = size // 2# 按照 gap 分组while gap > 0:# 对每组元素进行插入排序for i in range(gap, size):# temp 为每组中无序数组第 1 个元素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 = gap // 2return numsdef sortArray(self, nums: [int]) -> [int]:return self.shellSort(nums)

4. 希尔排序算法分析

  • 时间复杂度:介于 O ( n × log ⁡ 2 n ) O(n \times \log^2 n) O(n×log2n) O ( n 2 ) O(n^2) O(n2) 之间。

    • 希尔排序方法的速度是一系列间隔数 g a p i gap_i gapi 的函数,而比较次数与 g a p i gap_i gapi 之间的依赖关系比较复杂,不太容易给出完整的数学分析。
    • 本文采用 g a p i = ⌊ g a p i − 1 / 2 ⌋ gap_i = \lfloor gap_{i-1}/2 \rfloor gapi=gapi1/2 的方法缩小间隔数,对于具有 n n n 个元素的数组,如果 g a p 1 = ⌊ n / 2 ⌋ gap_1 = \lfloor n/2 \rfloor gap1=n/2,则经过 p = ⌊ log ⁡ 2 n ⌋ p = \lfloor \log_2 n \rfloor p=log2n 趟排序后就有 g a p p = 1 gap_p = 1 gapp=1,因此,希尔排序方法的排序总躺数为 ⌊ log ⁡ 2 n ⌋ \lfloor \log_2 n \rfloor log2n
    • 从算法中也可以看到,外层 while gap > 0 的循环次数为 log ⁡ n \log n logn 数量级,内层插入排序算法循环次数为 n n n 数量级。当子数组分得越多时,子数组内的元素就越少,内层循环的次数也就越少;反之,当所分的子数组个数减少时,子数组内的元素也随之增多,但整个数组也逐步接近有序,而循环次数却不会随之增加。因此,希尔排序算法的时间复杂度在 O ( n × log ⁡ 2 n ) O(n \times \log^2 n) O(n×log2n) O ( n 2 ) O(n^2) O(n2) 之间。
  • 空间复杂度 O ( 1 ) O(1) O(1)。希尔排序中用到的插入排序算法为原地排序算法,只用到指针变量 i i i j j j 以及表示无序区间中第 1 1 1 个元素的变量、间隔数 g a p gap gap 等常数项的变量。

  • 排序稳定性:在一次插入排序是稳定的,不会改变相等元素的相对顺序,但是在不同的插入排序中,相等元素可能在各自的插入排序中移动。因此,希尔排序方法是一种 不稳定排序算法

七. 练习题目2

4. 0506. 相对名次

4.1 题目大意

描述:给定一个长度为 n 的数组 score。其中 score[i] 表示第 i 名运动员在比赛中的成绩。所有成绩互不相同。

要求:找出他们的相对名次,并授予前三名对应的奖牌。前三名运动员将会被分别授予「金牌(Gold Medal)」,「银牌(Silver Medal)」和「铜牌(Bronze Medal)」。

说明

  • n = = s c o r e . l e n g t h n == score.length n==score.length
  • 1 ≤ n ≤ 1 0 4 1 \le n \le 10^4 1n104
  • 0 ≤ s c o r e [ i ] ≤ 1 0 6 0 \le score[i] \le 10^6 0score[i]106
  • score 中的所有值互不相同。

示例

输入:score = [5,4,3,2,1]
输出:["Gold Medal","Silver Medal","Bronze Medal","4","5"]
解释:名次为 [1st, 2nd, 3rd, 4th, 5th] 。输入:score = [10,3,8,9,4]
输出:["Gold Medal","5","Bronze Medal","Silver Medal","4"]
解释:名次为 [1st, 5th, 3rd, 2nd, 4th]

4.2 解题思路

思路 1:排序
  1. 先对数组 score 进行排序。
  2. 再将对应前三个位置上的元素替换成对应的字符串:Gold Medal, Silver Medal, Bronze Medal
思路 1:代码
class Solution:def findRelativeRanks(self, score: List[int]) -> List[str]:mark=("Gold Medal", "Silver Medal", "Bronze Medal")for i, j in enumerate(sorted(range(len(score)), key=lambda x: -score[x])):score[j] = str(i + 1) if i > 2 else mark[i]return score
思路 1:复杂度分析
  • 时间复杂度 O ( n × log ⁡ 2 n ) O(n \times \log_2n) O(n×log2n)。因为采用了时间复杂度为 O ( n × log ⁡ 2 n ) O(n \times \log_2n) O(n×log2n) 的快速排序。
  • 空间复杂度 O ( n ) O(n) O(n)

5. 0088. 合并两个有序数组

5.1 题目大意

描述:给定两个有序数组 n u m s 1 nums1 nums1 n u m s 2 nums2 nums2

要求:将 n u m s 2 nums2 nums2 合并到 n u m s 1 nums1 nums1 中,使 n u m s 1 nums1 nums1 成为一个有序数组。

说明

  • 给定数组 n u m s 1 nums1 nums1 空间大小为 m + n m + n m+n 个,其中前 m m m 个为 n u m s 1 nums1 nums1 的元素。 n u m s 2 nums2 nums2 空间大小为 n n n。这样可以用 n u m s 1 nums1 nums1 的空间来存储最终的有序数组。
  • n u m s 1. l e n g t h = = m + n nums1.length == m + n nums1.length==m+n
  • n u m s 2. l e n g t h = = n nums2.length == n nums2.length==n
  • 0 ≤ m , n ≤ 200 0 \le m, n \le 200 0m,n200
  • 1 ≤ m + n ≤ 200 1 \le m + n \le 200 1m+n200
  • − 1 0 9 ≤ n u m s 1 [ i ] , n u m s 2 [ j ] ≤ 1 0 9 -10^9 \le nums1[i], nums2[j] \le 10^9 109nums1[i],nums2[j]109

示例

  • 示例 1:
输入: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] ,其中斜体加粗标注的为 nums1 中的元素。
  • 示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1][] 。
合并结果是 [1]

5.2 解题思路

思路 1:快慢指针
  1. 将两个指针 index1index2 分别指向 nums1nums2 数组的尾部,再用一个指针 index 指向数组 nums1 的尾部。
  2. 从后向前判断当前指针下 nums1[index1]nums[index2] 的值大小,将较大值存入 num1[index] 中,然后继续向前遍历。
  3. 最后再将 nums2 中剩余元素赋值到 num1 前面对应位置上。
思路 1:代码
class Solution:def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None:m, n = m - 1, 1while nums2:if m >= 0 and nums1[m] > nums2[-1]:nums1[-n] = nums1[m]m -= 1else:nums1[-n] = nums2.pop()n += 1
思路 1:复杂度分析
  • 时间复杂度 O ( m + n ) O(m + n) O(m+n)
  • 空间复杂度 O ( m + n ) O(m + n) O(m+n)

6. 剑指 Offer 51. 数组中的逆序对

6.1 题目大意

描述:给定一个数组 nums

要求:计算出数组中的逆序对的总数。

说明

  • 逆序对:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
  • 0 ≤ n u m s . l e n g t h ≤ 50000 0 \le nums.length \le 50000 0nums.length50000

示例

输入: [7,5,6,4]
输出: 5

6.2 解题思路

思路 1:树状数组

数组 tree[i] 表示数字 i 是否在序列中出现过,如果数字 i 已经存在于序列中,tree[i] = 1,否则 tree[i] = 0

  1. 按序列从左到右将值为 nums[i] 的元素当作下标为nums[i],赋值为 1 插入树状数组里,这时,比 nums[i] 大的数个数就是 i + 1 - query(a)
  2. 将全部结果累加起来就是逆序数了。
思路 1:代码
import bisectclass BinaryIndexTree:def __init__(self, n):self.size = nself.tree = [0 for _ in range(n + 1)]def lowbit(self, index):return index & (-index)def update(self, index, delta):while index <= self.size:self.tree[index] += deltaindex += self.lowbit(index)def query(self, index):res = 0while index > 0:res += self.tree[index]index -= self.lowbit(index)return resclass Solution:def reversePairs(self, nums: List[int]) -> int:size = len(nums)sort_nums = sorted(nums)for i in range(size):nums[i] = bisect.bisect_left(sort_nums, nums[i]) + 1bit = BinaryIndexTree(size)ans = 0for i in range(size):bit.update(nums[i], 1)ans += (i + 1 - bit.query(nums[i]))return ans
思路 1:复杂度分析
  • 时间复杂度 O ( n × log ⁡ 2 n ) O(n \times \log_2n) O(n×log2n)
  • 空间复杂度 O ( n ) O(n) O(n)

八. 快速排序

1. 快速排序算法思想

快速排序(Quick Sort)基本思想

采用经典的分治策略,选择数组中某个元素作为基准数,通过一趟排序将数组分为独立的两个子数组,一个子数组中所有元素值都比基准数小,另一个子数组中所有元素值都比基准数大。然后再按照同样的方式递归的对两个子数组分别进行快速排序,以达到整个数组有序。

2. 快速排序算法步骤

假设数组的元素个数为 n n n 个,则快速排序的算法步骤如下:

  1. 哨兵划分:选取一个基准数,将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。
    1. 从当前数组中找到一个基准数 p i v o t pivot pivot(这里以当前数组第 1 1 1 个元素作为基准数,即 p i v o t = n u m s [ l o w ] pivot = nums[low] pivot=nums[low])。
    2. 使用指针 i i i 指向数组开始位置,指针 j j j 指向数组末尾位置。
    3. 从右向左移动指针 j j j,找到第 1 1 1 个小于基准值的元素。
    4. 从左向右移动指针 i i i,找到第 1 1 1 个大于基准数的元素。
    5. 交换指针 i i i、指针 j j j 指向的两个元素位置。
    6. 重复第 3 ∼ 5 3 \sim 5 35 步,直到指针 i i i 和指针 j j j 相遇时停止,最后将基准数放到两个子数组交界的位置上。
  2. 递归分解:完成哨兵划分之后,对划分好的左右子数组分别进行递归排序。
    1. 按照基准数的位置将数组拆分为左右两个子数组。
    2. 对每个子数组分别重复「哨兵划分」和「递归分解」,直到各个子数组只有 1 1 1 个元素,排序结束。

我们以 [ 4 , 7 , 5 , 2 , 6 , 1 , 3 ] [4, 7, 5, 2, 6, 1, 3] [4,7,5,2,6,1,3] 为例,演示一下快速排序的整个步骤。

我们先来看一下单次「哨兵划分」的过程。

<1>

在这里插入图片描述

<2>

在这里插入图片描述

<3>

在这里插入图片描述

<4>

在这里插入图片描述

<5>

在这里插入图片描述

<6>

在这里插入图片描述

<7>

在这里插入图片描述

在经过一次「哨兵划分」过程之后,数组就被划分为左子数组、基准数、右子树组三个独立部分。接下来只要对划分好的左右子数组分别进行递归排序即可完成排序。整个步骤如下:

在这里插入图片描述

3. 快速排序代码实现

import randomclass Solution:# 随机哨兵划分:从 nums[low: high + 1] 中随机挑选一个基准数,并进行移位排序def randomPartition(self, nums: [int], low: int, high: int) -> int:# 随机挑选一个基准数i = random.randint(low, high)# 将基准数与最低位互换nums[i], nums[low] = nums[low], nums[i]# 以最低位为基准数,然后将数组中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上return self.partition(nums, low, high)# 哨兵划分:以第 1 位元素 nums[low] 为基准数,然后将比基准数小的元素移动到基准数左侧,将比基准数大的元素移动到基准数右侧,最后将基准数放到正确位置上def partition(self, nums: [int], low: int, high: int) -> int:# 以第 1 位元素为基准数pivot = nums[low]i, j = low, highwhile i < j:# 从右向左找到第 1 个小于基准数的元素while i < j and nums[j] >= pivot:j -= 1# 从左向右找到第 1 个大于基准数的元素while i < j and nums[i] <= pivot:i += 1# 交换元素nums[i], nums[j] = nums[j], nums[i]# 将基准节点放到正确位置上nums[i], nums[low] = nums[low], nums[i]# 返回基准数的索引return idef quickSort(self, nums: [int], low: int, high: int) -> [int]:if low < high:# 按照基准数的位置,将数组划分为左右两个子数组pivot_i = self.randomPartition(nums, low, high)# 对左右两个子数组分别进行递归快速排序self.quickSort(nums, low, pivot_i - 1)self.quickSort(nums, pivot_i + 1, high)return numsdef sortArray(self, nums: [int]) -> [int]:return self.quickSort(nums, 0, len(nums) - 1)

4. 快速排序算法分析

快速排序算法的时间复杂度主要跟基准数的选择有关。本文中是将当前数组中第 1 1 1 个元素作为基准值。

在这种选择下,如果参加排序的元素初始时已经有序的情况下,快速排序方法花费的时间最长。也就是会得到最坏时间复杂度。

在这种情况下,第 1 1 1 趟排序经过 n − 1 n - 1 n1 次比较以后,将第 1 1 1 个元素仍然确定在原来的位置上,并得到 1 1 1 个长度为 n − 1 n - 1 n1 的子数组。第 2 2 2 趟排序进过 n − 2 n - 2 n2 次比较以后,将第 2 2 2 个元素确定在它原来的位置上,又得到 1 1 1 个长度为 n − 2 n - 2 n2 的子数组。

最终总的比较次数为 ( n − 1 ) + ( n − 2 ) + … + 1 = n ( n − 1 ) 2 (n − 1) + (n − 2) + … + 1 = \frac{n(n − 1)}{2} (n1)+(n2)++1=2n(n1)。因此这种情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2),也是最坏时间复杂度。

我们可以改进一下基准数的选择。如果每次我们选中的基准数恰好能将当前数组平分为两份,也就是刚好取到当前数组的中位数。

在这种选择下,每一次都将数组从 n n n 个元素变为 n 2 \frac{n}{2} 2n 个元素。此时的时间复杂度公式为 T ( n ) = 2 × T ( n 2 ) + Θ ( n ) T(n) = 2 \times T(\frac{n}{2}) + \Theta(n) T(n)=2×T(2n)+Θ(n)。根据主定理可以得出 T ( n ) = O ( n × log ⁡ n ) T(n) = O(n \times \log n) T(n)=O(n×logn),也是最佳时间复杂度。

而在平均情况下,我们可以从当前数组中随机选择一个元素作为基准数。这样,每一次选择的基准数可以看做是等概率随机的。其期望时间复杂度为 O ( n × log ⁡ n ) O(n \times \log n) O(n×logn),也就是平均时间复杂度。

下面来总结一下:

  • 最佳时间复杂度 O ( n × log ⁡ n ) O(n \times \log n) O(n×logn)。每一次选择的基准数都是当前数组的中位数,此时算法时间复杂度满足的递推式为 T ( n ) = 2 × T ( n 2 ) + Θ ( n ) T(n) = 2 \times T(\frac{n}{2}) + \Theta(n) T(n)=2×T(2n)+Θ(n),由主定理可得 T ( n ) = O ( n × log ⁡ n ) T(n) = O(n \times \log n) T(n)=O(n×logn)
  • 最坏时间复杂度 O ( n 2 ) O(n^2) O(n2)。每一次选择的基准数都是数组的最终位置上的值,此时算法时间复杂度满足的递推式为 T ( n ) = T ( n − 1 ) + Θ ( n ) T(n) = T(n - 1) + \Theta(n) T(n)=T(n1)+Θ(n),累加可得 T ( n ) = O ( n 2 ) T(n) = O(n^2) T(n)=O(n2)
  • 平均时间复杂度 O ( n × log ⁡ n ) O(n \times \log n) O(n×logn)。在平均情况下,每一次选择的基准数可以看做是等概率随机的。其期望时间复杂度为 O ( n × log ⁡ n ) O(n \times \log n) O(n×logn)
  • 空间复杂度 O ( n ) O(n) O(n)。无论快速排序算法递归与否,排序过程中都需要用到堆栈或其他结构的辅助空间来存放当前待排序数组的首、尾位置。最坏的情况下,空间复杂度为 O ( n ) O(n) O(n)。如果对算法进行一些改写,在一趟排序之后比较被划分所得到的两个子数组的长度,并且首先对长度较短的子数组进行快速排序,这时候需要的空间复杂度可以达到 O ( l o g 2 n ) O(log_2 n) O(log2n)
  • 排序稳定性:在进行哨兵划分时,基准数可能会被交换至相等元素的右侧。因此,快速排序是一种 不稳定排序算法

九. 堆排序

1. 堆结构

「堆排序(Heap sort)」是一种基于「堆结构」实现的高效排序算法。在介绍「堆排序」之前,我们先来了解一下什么是「堆结构」。

1.1 堆的定义

堆(Heap):一种满足以下两个条件之一的完全二叉树:

  • 大顶堆(Max Heap):任意节点值 ≥ 其子节点值。
  • 小顶堆(Min Heap):任意节点值 ≤ 其子节点值。

在这里插入图片描述

1.2 堆的存储结构

堆的逻辑结构就是一颗完全二叉树。而我们在「07.树 - 01.二叉树 - 01.树与二叉树的基础知识」章节中学过,对于完全二叉树(尤其是满二叉树)来说,采用顺序存储结构(数组)的形式来表示完全二叉树,能够充分利用存储空间。

当我们使用顺序存储结构(即数组)来表示堆时,堆中元素的节点编号与数组的索引关系为:

  • 如果某二叉树节点(非叶子节点)的下标为 i i i,那么其左孩子节点下标为 2 × i + 1 2 \times i + 1 2×i+1,右孩子节点下标为 2 × i + 2 2 \times i + 2 2×i+2
  • 如果某二叉树节点(非根结点)的下标为 i i i,那么其根节点下标为 ⌊ i − 1 2 ⌋ \lfloor \frac{i - 1}{2} \rfloor 2i1(向下取整)。
class MaxHeap:def __init__(self):self.max_heap = []

在这里插入图片描述

1.3 访问堆顶元素

访问堆顶元素:指的是从堆结构中获取位于堆顶的元素。

在堆中,堆顶元素位于根节点,当我们使用顺序存储结构(即数组)来表示堆时,堆顶元素就是数组的首个元素。

class MaxHeap:......def peek(self) -> int:# 大顶堆为空if not self.max_heap:return None# 返回堆顶元素return self.max_heap[0]

访问堆顶元素不依赖于数组中元素个数,因此时间复杂度为 O ( 1 ) O(1) O(1)

1.4 向堆中插入元素

向堆中插入元素:指的将一个新的元素添加到堆中,调整堆结构,以保持堆的特性不变。

向堆中插入元素的步骤如下:

  1. 将新元素添加到堆的末尾,保持完全二叉树的结构。
  2. 从新插入的元素节点开始,将该节点与其父节点进行比较。
    1. 如果新节点的值大于其父节点的值,则交换它们,以保持最大堆的特性。
    2. 如果新节点的值小于等于其父节点的值,说明已满足最大堆的特性,此时结束。
  3. 重复上述比较和交换步骤,直到新节点不再大于其父节点,或者达到了堆的根节点。

这个过程称为「上移调整(Shift Up)」。因为新插入的元素会逐步向堆的上方移动,直到找到了合适的位置,保持堆的有序性。

<1>

在这里插入图片描述

<2>

在这里插入图片描述

<3>

在这里插入图片描述

<4>

在这里插入图片描述

<5>

在这里插入图片描述

<6>

在这里插入图片描述

<7>

在这里插入图片描述

class MaxHeap:......def push(self, val: int):# 将新元素添加到堆的末尾self.max_heap.append(val)size = len(self.max_heap)# 从新插入的元素节点开始,进行上移调整self.__shift_up(size - 1)def __shift_up(self, i: int):while (i - 1) // 2 >= 0 and self.max_heap[i] > self.max_heap[(i - 1) // 2]:self.max_heap[i], self.max_heap[(i - 1) // 2] = self.max_heap[(i - 1) // 2], self.max_heap[i]i = (i - 1) // 2

在最坏情况下,「向堆中插入元素」的时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn),其中 n n n 是堆中元素的数量,这是因为堆的高度是 log ⁡ n \log n logn

1.5 删除堆顶元素

删除堆顶元素:指的是从堆中移除位于堆顶的元素,并重新调整对结果,以保持堆的特性不变。

删除堆顶元素的步骤如下:

  1. 将堆顶元素(即根节点)与堆的末尾元素交换。
  2. 移除堆末尾的元素(之前的堆顶),即将其从堆中剔除。
  3. 从新的堆顶元素开始,将其与其较大的子节点进行比较。
    1. 如果当前节点的值小于其较大的子节点,则将它们交换。这一步是为了将新的堆顶元素「下沉」到适当的位置,以保持最大堆的特性。
    2. 如果当前节点的值大于等于其较大的子节点,说明已满足最大堆的特性,此时结束。
  4. 重复上述比较和交换步骤,直到新的堆顶元素不再小于其子节点,或者达到了堆的底部。

这个过程称为「下移调整(Shift Down)」。因为新的堆顶元素会逐步向堆的下方移动,直到找到了合适的位置,保持堆的有序性。

<1>

在这里插入图片描述

<2>

在这里插入图片描述

<3>

在这里插入图片描述

<4>

在这里插入图片描述

<5>

在这里插入图片描述

<6>

在这里插入图片描述

<7>

在这里插入图片描述

class MaxHeap:......        def pop(self) -> int:# 堆为空if not self.max_heap:raise IndexError("堆为空")size = len(self.max_heap)self.max_heap[0], self.max_heap[size - 1] = self.max_heap[size - 1], self.max_heap[0]# 删除堆顶元素val = self.max_heap.pop()# 节点数减 1size -= 1 # 下移调整self.__shift_down(0, size)# 返回堆顶元素return valdef __shift_down(self, i: int, n: int):while 2 * i + 1 < n:# 左右子节点编号left, right = 2 * i + 1, 2 * i + 2# 找出左右子节点中的较大值节点编号if 2 * i + 2 >= n:# 右子节点编号超出范围(只有左子节点larger = leftelse:# 左子节点、右子节点都存在if self.max_heap[left] >= self.max_heap[right]:larger = leftelse:larger = right# 将当前节点值与其较大的子节点进行比较if self.max_heap[i] < self.max_heap[larger]:# 如果当前节点值小于其较大的子节点,则将它们交换self.max_heap[i], self.max_heap[larger] = self.max_heap[larger], self.max_heap[i]i = largerelse:# 如果当前节点值大于等于于其较大的子节点,此时结束break

「删除堆顶元素」的时间复杂度通常为 O ( log ⁡ n ) O(\log n) O(logn),其中 n n n 是堆中元素的数量,因为堆的高度是 log ⁡ n \log n logn

2. 堆排序

2.1 堆排序算法思想

堆排序(Heap sort)基本思想

借用「堆结构」所设计的排序算法。将数组转化为大顶堆,重复从大顶堆中取出数值最大的节点,并让剩余的堆结构继续维持大顶堆性质。

2.2 堆排序算法步骤

  1. 构建初始大顶堆

    1. 定义一个数组实现的堆结构,将原始数组的元素依次存入堆结构的数组中(初始顺序不变)。
    2. 从数组的中间位置开始,从右至左,依次通过「下移调整」将数组转换为一个大顶堆。
  2. 交换元素,调整堆

    1. 交换堆顶元素(第 1 1 1 个元素)与末尾(最后 1 1 1 个元素)的位置,交换完成后,堆的长度减 1 1 1
    2. 交换元素之后,由于堆顶元素发生了改变,需要从根节点开始,对当前堆进行「下移调整」,使其保持堆的特性。
  3. 重复交换和调整堆

    1. 重复第 2 2 2 步,直到堆的大小为 1 1 1 时,此时大顶堆的数组已经完全有序。
2.2.1 构建初始大顶堆

<1>

在这里插入图片描述

<2>

在这里插入图片描述

<3>

在这里插入图片描述

<4>

在这里插入图片描述

<5>

在这里插入图片描述

<6>

在这里插入图片描述

<7>

在这里插入图片描述

2.2.2 交换元素,调整堆

<1>

在这里插入图片描述

<2>

在这里插入图片描述

<3>

在这里插入图片描述

<4>

在这里插入图片描述

<5>

在这里插入图片描述

<6>

在这里插入图片描述

<7>

在这里插入图片描述

<8>

在这里插入图片描述

<9>

在这里插入图片描述

<10>

在这里插入图片描述

<11>

在这里插入图片描述

<12>

在这里插入图片描述

2.3 堆排序代码实现

class MaxHeap:......def __buildMaxHeap(self, nums: [int]):size = len(nums)# 先将数组 nums 的元素按顺序添加到 max_heap 中for i in range(size):self.max_heap.append(nums[i])# 从最后一个非叶子节点开始,进行下移调整for i in range((size - 2) // 2, -1, -1):self.__shift_down(i, size)def maxHeapSort(self, nums: [int]) -> [int]:# 根据数组 nums 建立初始堆self.__buildMaxHeap(nums)size = len(self.max_heap)for i in range(size - 1, -1, -1):# 交换根节点与当前堆的最后一个节点self.max_heap[0], self.max_heap[i] = self.max_heap[i], self.max_heap[0]# 从根节点开始,对当前堆进行下移调整self.__shift_down(0, i)# 返回排序后的数组return self.max_heapclass Solution:def maxHeapSort(self, nums: [int]) -> [int]:return MaxHeap().maxHeapSort(nums)def sortArray(self, nums: [int]) -> [int]:return self.maxHeapSort(nums)print(Solution().sortArray([10, 25, 6, 8, 7, 1, 20, 23, 16, 19, 17, 3, 18, 14]))

2.4 堆排序算法分析

  • 时间复杂度 O ( n × log ⁡ n ) O(n \times \log n) O(n×logn)
    • 堆积排序的时间主要花费在两个方面:「建立初始堆」和「下移调整」。
    • 设原始数组所对应的完全二叉树深度为 d d d,算法由两个独立的循环组成:
      1. 在第 1 1 1 个循环构造初始堆积时,从 i = d − 1 i = d - 1 i=d1 层开始,到 i = 1 i = 1 i=1 层为止,对每个分支节点都要调用一次调整堆算法,而一次调整堆算法,对于第 i i i 层一个节点到第 d d d 层上建立的子堆积,所有节点可能移动的最大距离为该子堆积根节点移动到最后一层(第 d d d 层) 的距离,即 d − i d - i di。而第 i i i 层上节点最多有 2 i − 1 2^{i-1} 2i1 个,所以每一次调用调整堆算法的最大移动距离为 2 i − 1 ∗ ( d − i ) 2^{i-1} * (d-i) 2i1(di)。因此,堆积排序算法的第 1 1 1 个循环所需时间应该是各层上的节点数与该层上节点可移动的最大距离之积的总和,即: ∑ i = d − 1 1 2 i − 1 ( d − i ) = ∑ j = 1 d − 1 2 d − j − 1 × j = ∑ j = 1 d − 1 2 d − 1 × j 2 j ≤ n × ∑ j = 1 d − 1 j 2 j < 2 × n \sum_{i = d - 1}^1 2^{i-1} (d-i) = \sum_{j = 1}^{d-1} 2^{d-j-1} \times j = \sum_{j = 1}^{d-1} 2^{d-1} \times {j \over 2^j} \le n \times \sum_{j = 1}^{d-1} {j \over 2^j} < 2 \times n i=d112i1(di)=j=1d12dj1×j=j=1d12d1×2jjn×j=1d12jj<2×n。这一部分的时间花费为 O ( n ) O(n) O(n)
      2. 在第 2 2 2 个循环中,每次调用调整堆算法一次,节点移动的最大距离为这棵完全二叉树的深度 d = ⌊ log ⁡ 2 ( n ) ⌋ + 1 d = \lfloor \log_2(n) \rfloor + 1 d=log2(n)⌋+1,一共调用了 n − 1 n - 1 n1 次调整堆算法,所以,第 2 2 2 个循环的时间花费为 ( n − 1 ) ( ⌊ log ⁡ 2 ( n ) ⌋ + 1 ) = O ( n × log ⁡ n ) (n-1)(\lfloor \log_2 (n)\rfloor + 1) = O(n \times \log n) (n1)(⌊log2(n)⌋+1)=O(n×logn)
    • 因此,堆积排序的时间复杂度为 O ( n × log ⁡ n ) O(n \times \log n) O(n×logn)
  • 空间复杂度 O ( 1 ) O(1) O(1)。由于在堆积排序中只需要一个记录大小的辅助空间,因此,堆积排序的空间复杂度为: O ( 1 ) O(1) O(1)
  • 排序稳定性:在进行「下移调整」时,相等元素的相对位置可能会发生变化。因此,堆排序是一种 不稳定排序算法

十. 练习题目3

7. 0075. 颜色分类

7.1 题目大意

描述:给定一个数组 nums,元素值只有 012,分别代表红色、白色、蓝色。

要求:将数组进行排序,使得红色在前,白色在中间,蓝色在最后。

说明

  • 要求不使用标准库函数,同时仅用常数空间,一趟扫描解决。
  • n = = n u m s . l e n g t h n == nums.length n==nums.length
  • 1 ≤ n ≤ 300 1 \le n \le 300 1n300
  • nums[i]012

示例

输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]输入:nums = [2,0,1]
输出:[0,1,2]

7.2 解题思路

思路 1:双指针 + 快速排序思想

快速排序算法中的 partition 过程,利用双指针,将序列中比基准数 pivot 大的元素移动到了基准数右侧,将比基准数 pivot 小的元素移动到了基准数左侧。从而将序列分为了三部分:比基准数小的部分、基准数、比基准数大的部分。

这道题我们也可以借鉴快速排序算法中的 partition 过程,将 1 作为基准数 pivot,然后将序列分为三部分:0(即比 1 小的部分)、等于 1 的部分、2(即比 1 大的部分)。具体步骤如下:

  1. 使用两个指针 leftright,分别指向数组的头尾。left 表示当前处理好红色元素的尾部,right 表示当前处理好蓝色的头部。
  2. 再使用一个下标 index 遍历数组,如果遇到 nums[index] == 0,就交换 nums[index]nums[left],同时将 left 右移。如果遇到 nums[index] == 2,就交换 nums[index]nums[right],同时将 right 左移。
  3. 直到 index 移动到 right 位置之后,停止遍历。遍历结束之后,此时 left 左侧都是红色,right 右侧都是蓝色。

注意:移动的时候需要判断 indexleft 的位置,因为 left 左侧是已经处理好的数组,所以需要判断 index 的位置是否小于 left,小于的话,需要更新 index 位置。

思路 1:代码
class Solution:def sortColors(self, nums: List[int]) -> None:n = len(nums)p0, p2 = 0, n - 1i = 0while i <= p2:while i <= p2 and nums[i] == 2:nums[i], nums[p2] = nums[p2], nums[i]p2 -= 1if nums[i] == 0:nums[i], nums[p0] = nums[p0], nums[i]p0 += 1i += 1
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( 1 ) O(1) O(1)

8. 0215. 数组中的第K个最大元素

8.1 题目大意

描述:给定一个未排序的整数数组 nums 和一个整数 k

要求:返回数组中第 k 个最大的元素。

说明

  • 要求使用时间复杂度为 O ( n ) O(n) O(n) 的算法解决此问题。
  • 1 ≤ k ≤ n u m s . l e n g t h ≤ 1 0 5 1 \le k \le nums.length \le 10^5 1knums.length105
  • − 1 0 4 ≤ n u m s [ i ] ≤ 1 0 4 -10^4 \le nums[i] \le 10^4 104nums[i]104

示例

输入: [3,2,1,5,6,4], k = 2
输出: 5输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

8.2 解题思路

思路 1:快速排序

使用快速排序在每次调整时,都会确定一个元素的最终位置,且以该元素为界限,将数组分成了左右两个子数组,左子数组中的元素都比该元素小,右子树组中的元素都比该元素大。

这样,只要某次划分的元素恰好是第 k 个下标就找到了答案。并且我们只需关注第 k 个最大元素所在区间的排序情况,与第 k 个最大元素无关的区间排序都可以忽略。这样进一步减少了执行步骤。

思路 1:代码
import randomclass Solution:# 从 arr[low: high + 1] 中随机挑选一个基准数,并进行移动排序def randomPartition(self, arr: [int], low: int, high: int):# 随机挑选一个基准数i = random.randint(low, high)# 将基准数与最低位互换arr[i], arr[low] = arr[low], arr[i]# 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上return self.partition(arr, low, high)# 以最低位为基准数,然后将序列中比基准数大的元素移动到基准数右侧,比他小的元素移动到基准数左侧。最后将基准数放到正确位置上def partition(self, arr: [int], low: int, high: int):pivot = arr[low]            # 以第 1 为为基准数i = low + 1                 # 从基准数后 1 位开始遍历,保证位置 i 之前的元素都小于基准数for j in range(i, high + 1):# 发现一个小于基准数的元素if arr[j] < pivot:# 将小于基准数的元素 arr[j] 与当前 arr[i] 进行换位,保证位置 i 之前的元素都小于基准数arr[i], arr[j] = arr[j], arr[i]# i 之前的元素都小于基准数,所以 i 向右移动一位i += 1# 将基准节点放到正确位置上arr[i - 1], arr[low] = arr[low], arr[i - 1]# 返回基准数位置return i - 1def quickSort(self, arr, low, high, k):size = len(arr)if low < high:# 按照基准数的位置,将序列划分为左右两个子序列pi = self.randomPartition(arr, low, high)if pi == size - k:return arr[size - k]if pi > size - k:# 对左子序列进行递归快速排序self.quickSort(arr, low, pi - 1, k)if pi < size - k:# 对右子序列进行递归快速排序self.quickSort(arr, pi + 1, high, k)return arr[size - k]def findKthLargest(self, nums: List[int], k: int) -> int:return self.quickSort(nums, 0, len(nums) - 1, k)
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)。证明过程可参考「算法导论 9.2:期望为线性的选择算法」。
  • 空间复杂度 O ( log ⁡ 2 n ) O(\log_2 n) O(log2n)。递归使用栈空间的空间代价期望为 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)

9. 剑指 Offer 40. 最小的k个数

9.1 题目大意

描述:给定整数数组 arr,再给定一个整数 k

要求:返回数组 arr 中最小的 k 个数。

说明

  • 0 ≤ k ≤ a r r . l e n g t h ≤ 10000 0 \le k \le arr.length \le 10000 0karr.length10000
  • 0 ≤ a r r [ i ] ≤ 10000 0 \le arr[i] \le 10000 0arr[i]10000

示例

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]输入:arr = [0,1,2,1], k = 1
输出:[0]

9.2 解题思路

思路 1:排序

对原数组从小到大排序后取出前 k个数即可。

思路 1:代码

class Solution:def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:arr.sort()return arr[:k]

思路 1:复杂度分析

  • 时间复杂度 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)
  • 空间复杂度 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)

十一. 计数排序

计数排序(Counting Sort)基本思想

通过统计数组中每个元素在数组中出现的次数,根据这些统计信息将数组元素有序的放置到正确位置,从而达到排序的目的。

2. 计数排序算法步骤

  1. 计算排序范围:遍历数组,找出待排序序列中最大值元素 n u m s ‾ m a x nums\underline{}max numsmax 和最小值元素 n u m s ‾ m i n nums\underline{}min numsmin,计算出排序范围为 n u m s ‾ m a x − n u m s ‾ m i n + 1 nums\underline{}max - nums\underline{}min + 1 numsmaxnumsmin+1

  2. 定义计数数组:定义一个大小为排序范围的计数数组 c o u n t s counts counts,用于统计每个元素的出现次数。其中:

    1. 数组的索引值 n u m − n u m s ‾ m i n num - nums\underline{}min numnumsmin 表示元素的值为 n u m num num
    2. 数组的值 c o u n t s [ n u m − n u m s ‾ m i n ] counts[num - nums\underline{}min] counts[numnumsmin] 表示元素 n u m num num 的出现次数。
  3. 对数组元素进行计数统计:遍历待排序数组 n u m s nums nums,对每个元素在计数数组中进行计数,即将待排序数组中「每个元素值减去最小值」作为索引,将「对计数数组中的值」加 1 1 1,即令 c o u n t s [ n u m − n u m s ‾ m i n ] counts[num - nums\underline{}min] counts[numnumsmin] 1 1 1

  4. 生成累积计数数组:从 c o u n t s counts counts 中的第 1 1 1 个元素开始,每一项累家前一项和。此时 c o u n t s [ n u m − n u m s ‾ m i n ] counts[num - nums\underline{}min] counts[numnumsmin] 表示值为 n u m num num 的元素在排序数组中最后一次出现的位置。

  5. 逆序填充目标数组:逆序遍历数组 n u m s nums nums,将每个元素 n u m num num 填入正确位置。

  6. 将其填充到结果数组 r e s res res 的索引 c o u n t s [ n u m − n u m s ‾ m i n ] counts[num - nums\underline{}min] counts[numnumsmin] 处。

  7. 放入后,令累积计数数组中对应索引减 1 1 1,从而得到下个元素 n u m num num 的放置位置。

我们以 [ 3 , 0 , 4 , 2 , 5 , 1 , 3 , 1 , 4 , 5 ] [3, 0, 4, 2, 5, 1, 3, 1, 4, 5] [3,0,4,2,5,1,3,1,4,5] 为例,演示一下计数排序的整个步骤。

在这里插入图片描述

3. 计数排序代码实现

class Solution:def countingSort(self, nums: [int]) -> [int]:# 计算待排序数组中最大值元素 nums_max 和最小值元素 nums_minnums_min, nums_max = min(nums), max(nums)# 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1size = nums_max - nums_min + 1counts = [0 for _ in range(size)]# 统计值为 num 的元素出现的次数for num in nums:counts[num - nums_min] += 1# 生成累积计数数组for i in range(1, size):counts[i] += counts[i - 1]# 反向填充目标数组res = [0 for _ in range(len(nums))]for i in range(len(nums) - 1, -1, -1):num = nums[i]# 根据累积计数数组,将 num 放在数组对应位置res[counts[num - nums_min] - 1] = num# 将 num 的对应放置位置减 1,从而得到下个元素 num 的放置位置counts[nums[i] - nums_min] -= 1return resdef sortArray(self, nums: [int]) -> [int]:return self.countingSort(nums)

4. 计数排序算法分析

  • 时间复杂度 O ( n + k ) O(n + k) O(n+k)。其中 k k k 代表待排序数组的值域。
  • 空间复杂度 O ( k ) O(k) O(k)。其中 k k k 代表待排序序列的值域。由于用于计数的数组 c o u n t s counts counts 的长度取决于待排序数组中数据的范围(大小等于待排序数组最大值减去最小值再加 1 1 1)。所以计数排序算法对于数据范围很大的数组,需要大量的内存。
  • 计数排序适用情况:计数排序一般用于整数排序,不适用于按字母顺序、人名顺序排序。
  • 排序稳定性:由于向结果数组中填充元素时使用的是逆序遍历,可以避免改变相等元素之间的相对顺序。因此,计数排序是一种 稳定排序算法

十二. 桶排序

1. 桶排序算法思想

桶排序(Bucket Sort)基本思想

将待排序数组中的元素分散到若干个「桶」中,然后对每个桶中的元素再进行单独排序。

2. 桶排序算法步骤

  1. 确定桶的数量:根据待排序数组的值域范围,将数组划分为 k k k 个桶,每个桶可以看做是一个范围区间。
  2. 分配元素:遍历待排序数组元素,将每个元素根据大小分配到对应的桶中。
  3. 对每个桶进行排序:对每个非空桶内的元素单独排序(使用插入排序、归并排序、快排排序等算法)。
  4. 合并桶内元素:将排好序的各个桶中的元素按照区间顺序依次合并起来,形成一个完整的有序数组。

我们以 [ 39 , 49 , 8 , 13 , 22 , 15 , 10 , 30 , 5 , 44 ] [39, 49, 8, 13, 22, 15, 10, 30, 5, 44] [39,49,8,13,22,15,10,30,5,44] 为例,演示一下桶排序的整个步骤。

在这里插入图片描述

3. 桶排序代码实现

class Solution:def insertionSort(self, nums: [int]) -> [int]:# 遍历无序区间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] = tempreturn numsdef bucketSort(self,  nums: [int], bucket_size=5) -> [int]:# 计算待排序序列中最大值元素 nums_max、最小值元素 nums_minnums_min, nums_max = min(nums), max(nums)# 定义桶的个数为 (最大值元素 - 最小值元素) // 每个桶的大小 + 1bucket_count = (nums_max - nums_min) // bucket_size + 1# 定义桶数组 bucketsbuckets = [[] for _ in range(bucket_count)]# 遍历待排序数组元素,将每个元素根据大小分配到对应的桶中for num in nums:buckets[(num - nums_min) // bucket_size].append(num)# 对每个非空桶内的元素单独排序,排序之后,按照区间顺序依次合并到 res 数组中res = []for bucket in buckets:self.insertionSort(bucket)res.extend(bucket)# 返回结果数组return resdef sortArray(self, nums: [int]) -> [int]:return self.bucketSort(nums)

4. 桶排序算法分析

  • 时间复杂度 O ( n ) O(n) O(n)。当输入元素个数为 n n n,桶的个数是 m m m 时,每个桶里的数据就是 k = n m k = \frac{n}{m} k=mn 个。每个桶内排序的时间复杂度为 O ( k × log ⁡ k ) O(k \times \log k) O(k×logk) m m m 个桶就是 m × O ( k × log ⁡ k ) = m × O ( n m × log ⁡ n m ) = O ( n × log ⁡ n m ) m \times O(k \times \log k) = m \times O(\frac{n}{m} \times \log \frac{n}{m}) = O(n \times \log \frac{n}{m}) m×O(k×logk)=m×O(mn×logmn)=O(n×logmn)。当桶的个数 m m m 接近于数据个数 n n n 时, log ⁡ n m \log \frac{n}{m} logmn 就是一个较小的常数,所以排序桶排序时间复杂度接近于 O ( n ) O(n) O(n)
  • 空间复杂度 O ( n + m ) O(n + m) O(n+m)。由于桶排序使用了辅助空间,所以桶排序的空间复杂度是 O ( n + m ) O(n + m) O(n+m)
  • 排序稳定性:桶排序的稳定性取决于桶内使用的排序算法。如果桶内使用稳定的排序算法(比如插入排序算法),并且在合并桶的过程中保持相等元素的相对顺序不变,则桶排序是一种 稳定排序算法。反之,则桶排序是一种 不稳定排序算法

十三. 基数排序

1. 基数排序算法思想

基数排序(Radix Sort)基本思想

将整数按位数切割成不同的数字,然后从低位开始,依次到高位,逐位进行排序,从而达到排序的目的。

2. 基数排序算法步骤

基数排序算法可以采用「最低位优先法(Least Significant Digit First)」或者「最高位优先法(Most Significant Digit first)」。最常用的是「最低位优先法」。

下面我们以最低位优先法为例,讲解一下算法步骤。

  1. 确定排序的最大位数:遍历数组元素,获取数组最大值元素,并取得对应位数。
  2. 从最低位(个位)开始,到最高位为止,逐位对每一位进行排序
    1. 定义一个长度为 10 10 10 的桶数组 b u c k e t s buckets buckets,每个桶分别代表 0 ∼ 9 0 \sim 9 09 中的 1 1 1 个数字。
    2. 按照每个元素当前位上的数字,将元素放入对应数字的桶中。
    3. 清空原始数组,然后按照桶的顺序依次取出对应元素,重新加入到原始数组中。

我们以 [ 692 , 924 , 969 , 503 , 871 , 704 , 542 , 436 ] [692, 924, 969, 503, 871, 704, 542, 436] [692,924,969,503,871,704,542,436] 为例,演示一下基数排序的整个步骤。

在这里插入图片描述

3. 基数排序代码实现

class Solution:def radixSort(self, nums: [int]) -> [int]:# 桶的大小为所有元素的最大位数size = len(str(max(nums)))# 从最低位(个位)开始,逐位遍历每一位for i in range(size):# 定义长度为 10 的桶数组 buckets,每个桶分别代表 0 ~ 9 中的 1 个数字。buckets = [[] for _ in range(10)]# 遍历数组元素,按照每个元素当前位上的数字,将元素放入对应数字的桶中。for num in nums:buckets[num // (10 ** i) % 10].append(num)# 清空原始数组nums.clear()# 按照桶的顺序依次取出对应元素,重新加入到原始数组中。for bucket in buckets:for num in bucket:nums.append(num)# 完成排序,返回结果数组return numsdef sortArray(self, nums: [int]) -> [int]:return self.radixSort(nums)

4. 基数排序算法分析

  • 时间复杂度 O ( n × k ) O(n \times k) O(n×k)。其中 n n n 是待排序元素的个数, k k k 是数字位数。 k k k 的大小取决于数字位的选择(十进制位、二进制位)和待排序元素所属数据类型全集的大小。
  • 空间复杂度 O ( n + k ) O(n + k) O(n+k)
  • 排序稳定性:基数排序采用的桶排序是稳定的。基数排序是一种 稳定排序算法

十四. 练习题目4

10. 1122. 数组的相对排序

10.1 题目大意

描述:给定两个数组,arr1arr2,其中 arr2 中的元素各不相同,arr2 中的每个元素都出现在 arr1 中。

要求:对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。

说明

  • 1 ≤ a r r 1. l e n g t h , a r r 2. l e n g t h ≤ 1000 1 \le arr1.length, arr2.length \le 1000 1arr1.length,arr2.length1000
  • 0 ≤ a r r 1 [ i ] , a r r 2 [ i ] ≤ 1000 0 \le arr1[i], arr2[i] \le 1000 0arr1[i],arr2[i]1000

示例

输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
输出:[2,2,2,1,4,3,3,9,6,7,19]输入:arr1 = [28,6,22,8,44,17], arr2 = [22,28,8,6]
输出:[22,28,8,6,17,44]

10.2 解题思路

思路 1:计数排序

因为元素值范围在 [0, 1000],所以可以使用计数排序的思路来解题。

  1. 使用数组 count 统计 arr1 各个元素个数。
  2. 遍历 arr2 数组,将对应元素num2 按照个数 count[num2] 添加到答案数组 ans 中,同时在 count 数组中减去对应个数。
  3. 然后在处理 count 中剩余元素,将 count 中大于 0 的元素下标依次添加到答案数组 ans 中。
  4. 最后返回答案数组 ans
思路 1:代码
class Solution:def relativeSortArray(self, arr1: List[int], arr2: List[int]) -> List[int]:# 计算待排序序列中最大值元素 arr_max 和最小值元素 arr_minarr1_min, arr1_max = min(arr1), max(arr1)# 定义计数数组 counts,大小为 最大值元素 - 最小值元素 + 1size = arr1_max - arr1_min + 1counts = [0 for _ in range(size)]# 统计值为 num 的元素出现的次数for num in arr1:counts[num - arr1_min] += 1res = []for num in arr2:while counts[num - arr1_min] > 0:res.append(num)counts[num - arr1_min] -= 1for i in range(size):while counts[i] > 0:num = i + arr1_minres.append(num)counts[i] -= 1return res
思路 1:复杂度分析
  • 时间复杂度 O ( m + n + m a x ( a r r 1 ) ) O(m + n + max(arr_1)) O(m+n+max(arr1))。其中 m m m 是数组 a r r 1 arr_1 arr1 的长度, n n n 是数组 a r r 2 arr_2 arr2 的长度, m a x ( a r r 1 ) max(arr_1) max(arr1) 是数组 a r r 1 arr_1 arr1 的最大值。
  • 空间复杂度 O ( m a x ( a r r 1 ) ) O(max(arr_1)) O(max(arr1))

11. 0220. 存在重复元素 III

11.1 题目大意

描述:给定一个整数数组 nums,以及两个整数 kt

要求:判断数组中是否存在两个不同下标的 ij,其对应元素满足 abs(nums[i] - nums[j]) <= t,同时满足 abs(i - j) <= k。如果满足条件则返回 True,不满足条件返回 False

说明

  • 0 ≤ n u m s . l e n g t h ≤ 2 ∗ 1 0 4 0 \le nums.length \le 2 * 10^4 0nums.length2104
  • − 2 31 ≤ n u m s [ i ] ≤ 2 31 − 1 -2^{31} \le nums[i] \le 2^{31} - 1 231nums[i]2311
  • 0 ≤ k ≤ 1 0 4 0 \le k \le 10^4 0k104
  • 0 ≤ t ≤ 2 31 − 1 0 \le t \le 2^{31} - 1 0t2311

示例

输入:nums = [1,2,3,1], k = 3, t = 0
输出:True输入:nums = [1,0,1,1], k = 1, t = 2
输出:True

11.2 解题思路

思路 1:滑动窗口(固定长度)
  1. 使用一个长度为 k 的滑动窗口,每次遍历到 nums[right] 时,滑动窗口内最多包含 nums[right] 之前最多 k 个元素。只需要检查前 k 个元素是否在 [nums[right] - t, nums[right] + t] 区间内即可。
  2. 检查 k 个元素是否在 [nums[right] - t, nums[right] + t] 区间,可以借助保证有序的数据结构(比如 SortedList)+ 二分查找来解决,从而减少时间复杂度。

具体步骤如下:

  1. 使用有序数组类 window 维护一个长度为 k 的窗口,满足数组内元素有序,且支持增加和删除操作。
  2. leftright 都指向序列的第一个元素。即:left = 0right = 0
  3. 将当前元素填入窗口中,即 window.add(nums[right])
  4. 当窗口元素大于 k 个时,即 right - left > k,移除窗口最左侧元素,并向右移动 left
  5. 当窗口元素小于等于 k 个时:
    1. 使用二分查找算法,查找 nums[right]window 中的位置 idx
    2. 判断 window[idx] 与相邻位置上元素差值绝对值,若果满足 abs(window[idx] - window[idx - 1]) <= t 或者 abs(window[idx + 1] - window[idx]) <= t 时返回 True
  6. 向右移动 right
  7. 重复 3 ~ 6 步,直到 right 到达数组末尾,如果还没找到满足条件的情况,则返回 False
思路 1:代码
from sortedcontainers import SortedListclass Solution:def containsNearbyAlmostDuplicate(self, nums: List[int], k: int, t: int) -> bool:size = len(nums)window = SortedList()left, right = 0, 0while right < size:window.add(nums[right])if right - left > k:window.remove(nums[left])left += 1idx = bisect.bisect_left(window, nums[right])if idx > 0 and abs(window[idx] - window[idx - 1]) <= t:return Trueif idx < len(window) - 1 and abs(window[idx + 1] - window[idx]) <= t:return Trueright += 1return False
思路 1:复杂度分析
  • 时间复杂度 O ( n × log ⁡ 2 ( m i n ( n , k ) ) ) O(n \times \log_2(min(n, k))) O(n×log2(min(n,k)))
  • 空间复杂度 O ( m i n ( n , k ) ) O(min(n, k)) O(min(n,k))

12. 0164. 最大间距

12.1 题目大意

描述:给定一个无序数组 nums

要求:找出数组在排序之后,相邻元素之间最大的差值。如果数组元素个数小于 2,则返回 0

说明

  • 所有元素都是非负整数,且数值在 32 位有符号整数范围内。
  • 请尝试在线性时间复杂度和空间复杂度的条件下解决此问题。

示例

输入: nums = [3,6,9,1]
输出: 3
解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6)(6,9) 之间都存在最大差值 3。输入: nums = [10]
输出: 0
解释: 数组元素个数小于 2,因此返回 0

12.2 解题思路

思路 1:基数排序

这道题的难点在于要求时间复杂度和空间复杂度为 O ( n ) O(n) O(n)

这道题分为两步:

  1. 数组排序。
  2. 计算相邻元素之间的差值。

第 2 步直接遍历数组求解即可,时间复杂度为 O ( n ) O(n) O(n)。所以关键点在于找到一个时间复杂度和空间复杂度为 O ( n ) O(n) O(n) 的排序算法。根据题意可知所有元素都是非负整数,且数值在 32 位有符号整数范围内。所以我们可以选择基数排序。基数排序的步骤如下:

  • 遍历数组元素,获取数组最大值元素,并取得位数。
  • 以个位元素为索引,对数组元素排序。
  • 合并数组。
  • 之后依次以十位,百位,…,直到最大值元素的最高位处值为索引,进行排序,并合并数组,最终完成排序。

最后,还要注意数组元素个数小于 2 的情况需要特别判断一下。

思路 1:代码
class Solution:def radixSort(self, arr):size = len(str(max(arr)))for i in range(size):buckets = [[] for _ in range(10)]for num in arr:buckets[num // (10 ** i) % 10].append(num)arr.clear()for bucket in buckets:for num in bucket:arr.append(num)return arrdef maximumGap(self, nums: List[int]) -> int:if len(nums) < 2:return 0arr = self.radixSort(nums)return max(arr[i] - arr[i - 1] for i in range(1, len(arr)))
思路 1:复杂度分析
  • 时间复杂度 O ( n ) O(n) O(n)
  • 空间复杂度 O ( n ) O(n) O(n)

十五. 排序算法题目

冒泡排序题目

题号标题题解标签难度
剑指 Offer 45把数组排成最小的数网页链接、Github 链接贪心、字符串、排序中等
0283移动零网页链接、Github 链接数组、双指针简单

选择排序题目

题号标题题解标签难度
0215数组中的第K个最大元素网页链接、Github 链接数组、分治、快速选择、排序、堆(优先队列)中等

插入排序题目

题号标题题解标签难度
0075颜色分类网页链接、Github 链接数组、双指针、排序中等

希尔排序题目

题号标题题解标签难度
0912排序数组网页链接、Github 链接数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序中等
0506相对名次网页链接、Github 链接数组、排序、堆(优先队列)简单

归并排序题目

题号标题题解标签难度
0912排序数组网页链接、Github 链接数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序中等
0088合并两个有序数组网页链接、Github 链接数组、双指针、排序简单
剑指 Offer 51数组中的逆序对网页链接、Github 链接树状数组、线段树、数组、二分查找、分治、有序集合、归并排序困难
0315计算右侧小于当前元素的个数树状数组、线段树、数组、二分查找、分治、有序集合、归并排序困难

快速排序题目

题号标题题解标签难度
0912排序数组网页链接、Github 链接数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序中等
0169多数元素网页链接、Github 链接数组、哈希表、分治、计数、排序简单

堆排序题目

题号标题题解标签难度
0912排序数组网页链接、Github 链接数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序中等
0215数组中的第K个最大元素网页链接、Github 链接数组、分治、快速选择、排序、堆(优先队列)中等
剑指 Offer 40最小的k个数网页链接、Github 链接数组、分治、快速选择、排序、堆(优先队列)简单

计数排序题目

题号标题题解标签难度
0912排序数组网页链接、Github 链接数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序中等
1122数组的相对排序网页链接、Github 链接数组、哈希表、计数排序、排序简单

桶排序题目

题号标题题解标签难度
0912排序数组网页链接、Github 链接数组、分治、桶排序、计数排序、基数排序、排序、堆(优先队列)、归并排序中等
0220存在重复元素 III网页链接、Github 链接数组、桶排序、有序集合、排序、滑动窗口困难
0164最大间距网页链接、Github 链接数组、桶排序、基数排序、排序困难

基数排序题目

题号标题题解标签难度
0164最大间距网页链接、Github 链接数组、桶排序、基数排序、排序困难
0561数组拆分贪心、数组、计数排序、排序简单

其他排序题目

题号标题题解标签难度
0217存在重复元素网页链接、Github 链接数组、哈希表、排序简单
0136只出现一次的数字网页链接、Github 链接位运算、数组简单
0056合并区间网页链接、Github 链接数组、排序中等
0179最大数网页链接、Github 链接贪心、数组、字符串、排序中等
0384打乱数组数组、数学、随机化中等
剑指 Offer 45把数组排成最小的数网页链接、Github 链接贪心、字符串、排序中等

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/86415.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SAP PO运维(一):系统概览异常处理

打开SAP PIPO Netweaver Administration界面,系统概览下显示异常: 参考SAP note: 2577844 - AS Java Monitoring and Logging parametrization best practice service/protectedwebmethods = SDEFAULT -GetVersionInfo -GetAccessPointList -ListLogFiles -ReadLogFile -Para…

为什么选择Spring Cloud

Spring Cloud与Netflix Netflix是一家做视频网站的公司&#xff0c;之所以要说一下这个公司是因为Spring Cloud在发展之初&#xff0c;Netflix做了很大的贡献。包括服务注册中心Eureka、服务调用Ribbon、Feign&#xff0c;服务容错限流Hystrix、服务网关Zuul等众多组件都是Net…

Linux下ThinkPHP5实现定时器任务 - 结合crontab

实例一&#xff1a; 1.在/application/command创建要配置的PHP类文件&#xff0c;需要继承Command类&#xff0c;并重写configure和execute两个方法&#xff0c;例如: <?php namespace app\command; use think\console\Command; use think\console\Input; use think\cons…

FatFS文件系统在MCU上的应用

FatFS文件系统是单片机领域有名的一个文件系统&#xff0c;由于它的轻量级和兼容性&#xff0c;备受MCU开发者青睐。 在实现如U盘文件读写&#xff0c;SD卡的文件读写等工作时&#xff0c;我们往往需要一个文件系统来支持我们的工作。特别在一些MCU应用中&#xff0c;文件系统…

PPPoE配置

实验需求 配置IP地址使用PPPOE拨号上网设置路由让直播业务部和营销部都可以访问外网 实验拓扑 实验步骤 配置 R1地址池 电信链路&#xff1a; [Huawei]undo info-center enable Info: Information center is disabled. [Huawei]sysname r1 [r1]ip pool zhibo  //配置…

【沐风老师】3DMAX翻转折叠动画插件FoldFx使用方法详解

3DMAX翻转折叠动画插件FoldFx使用方法详解 3DMAX翻转折叠动画插件FoldFx,是3dMax运动图形工具,用于创建多边形折叠动画。用户几乎有无限的可能性,因为动画的每个方面都是可控的。 【适用版本】 适用于3dMax版本:2010及更新版本(推荐3dMax2016及更高版本)。 【安装方法】…

Go 围炉札记

文章目录 一、Go 安装 一、Go 安装 VScode下配置Go语言开发环境【2023最新】 基础篇&#xff1a;新手使用vs code新建go项目 vscode里安装Go插件和配置Go环境 Documentation Golang 配置代理 Go命令详解 一文详解Go语言常用命令 Go 语言教程 熬夜整理&#xff0c;最全的Go语…

大数据-玩转数据-Flink SQL编程

一、概念 1.1 Apache Flink 两种关系型 API Apache Flink 有两种关系型 API 来做流批统一处理&#xff1a;Table API 和 SQL。 Table API 是用于 Scala 和 Java 语言的查询API&#xff0c;它可以用一种非常直观的方式来组合使用选取、过滤、join 等关系型算子。 Flink SQL 是…

CSS 学习笔记(基础)

用来控制网页表现的语言&#xff0c;CSS&#xff08;Cascading Style Sheet&#xff09;&#xff1a;层叠样式表。然后我们继续看看 W3C 标准&#xff1a; 结构&#xff1a;HTML表现&#xff1a;CSS行为&#xff1a;JavaScript CSS导入方式、选择器&属性 由于网页的框架…

linux进程杀不死

项目场景&#xff1a; 虚拟机 问题描述 linux进程杀不死 无反应 原因分析&#xff1a; 进程僵死zombie 解决方案&#xff1a; 进proc或者find命令找到进程所在地址 cat status查看进程杀死子进程

linux系统中mysql 连接出现“too many connections”问题解决办法

问题内容&#xff1a; 原因: mysql配置参数中设定的并发连接数太少或者系统繁忙导致连接数被占满。连接数超过了 MySQL 设置的值&#xff0c; 与 max_connections 和 wait timeout 都有关&#xff0c;wait_timeout 的值越大&#xff0c;连接的空闲等待就越长&#xff0c; 这样就…

Linux忘记密码

在虚拟机安装了centOS7&#xff0c;但是忘记了root密码&#xff0c;登录的时候发现登录不上了&#xff0c;然后重新修改密码。 1、重启虚拟机 2、进入到该页面之后&#xff0c;选中第一个&#xff08;高亮显示即为选中&#xff09;选项&#xff0c;然后按下键盘的“E”键 3…

mybatis日志体系

title: “java日志体系” createTime: 2021-12-08T12:19:5708:00 updateTime: 2021-12-08T12:19:5708:00 draft: false author: “ggball” tags: [“mybatis”] categories: [“java”] description: “java日志体系” java日志体系 常用日志框架 Log4j&#xff1a;Apache …

74、SpringBoot 整合 Spring Data JDBC

总结&#xff1a;用起来跟 Spring Data JPA 差不多 什么是 JdbcTemplate&#xff1f;&#xff08;Template译为模板&#xff09; Spring 框架对 JDBC 进行封装&#xff0c;使用 JdbcTemplate 方便实现对数据库操作 ★ Spring Data JDBC 既不需要JPA、Hibernate这种ORM框架&a…

离线部署 python 3.x 版本

文章目录 离线部署 python 3.x 版本1. 下载版本2. 上传到服务器3. 解压并安装4. 新建软连信息5. 注意事项 离线部署 python 3.x 版本 1. 下载版本 python 各版本下载地址 本次使用版本 Python-3.7.0a2.tgz # linux 可使用 wget 下载之后上传到所需服务器 wget https://www.py…

gma 2 成书计划

随着 gma 2 整体构建完成。下一步计划针对库内所有功能完成一个用户指南&#xff08;非网站&#xff09;。 封皮 主要章节 章节完成度相关链接第 1 章 GMA 概述已完成第 2 章 地理空间数据操作已完成第 3 章 坐标参考系统已完成第 4 章 地理空间制图已完成第 5 章 数学运算模…

3288S Android11 适配红外遥控功能(超详细)

目录 一、rk3288平台红外遥控介绍二、原理图分析三、配置设备树并使能红外遥控功能四、打开红外打印功能&#xff0c;查看红外遥控的用户码和键值五、将查看到的红外遥控用户码和键值添加到设备树和.kl文件六、Android红外遥控.kl文件映射知识和使用添加新的.kl文件七、补充&am…

Unity中关于多线程的一些事

一.线程中不允许调用unity组件api 解决方法&#xff1a;可以使用bool值变化并且在update中监测bool值变化来调用关于unity组件的API. 二.打印并且将信息输出到list列表中 多线程可能同时输出多条信息。输出字符串可以放入Queue队列中。队列可以被多线程插入。 三.启用socke…

Python 网络爬取的时候使用那种框架

尽管现代的网站多采取前后端分离的方式进行开发了&#xff0c;但是对直接 API 的调用我们通常会有 token 的限制和可以调用频率的限制。 因此&#xff0c;在一些特定的网站上&#xff0c;我们可能还是需要使用网络爬虫的方式获得已经返回的 JSON 数据结构&#xff0c;甚至是处理…

【计算机毕业设计】基于SpringBoot+Vue记帐理财系统的设计与实现

博主主页&#xff1a;一季春秋博主简介&#xff1a;专注Java技术领域和毕业设计项目实战、Java、微信小程序、安卓等技术开发&#xff0c;远程调试部署、代码讲解、文档指导、ppt制作等技术指导。主要内容&#xff1a;毕业设计(Java项目、小程序、安卓等)、简历模板、学习资料、…