LeetCode---排序

经典排序算法

参考文章:

  1. 十大经典排序算法(动图演示)
  2. 十大经典排序算法

1. 冒泡排序

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1 算法描述

  1. 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  3. 针对所有的元素重复以上的步骤,除了最后一个;
  4. 重复步骤1~3,直到排序完成。

1.2 代码实现

# 时间复杂度:0(n^2)
# 空间复杂度:O(1)

def bubble_sort(data):

    length = len(data)
    switch = False
    for i in range(length):
        for j in range(length-i-1):
            # 内层循环依次比较相邻的两个元素,一轮结束最大的元素排在末尾
            if data[j] > data[j+1]:
                data[j], data[j+1] = data[j+1], data[j]
                switch = True
        # 如果一轮结束没有发生交换说明已经排好序
        if not switch:
            break

    return data


if __name__ == '__main__':
    data = [3, 2, 1, 5, 6, 4]
    print(bubble_sort(data))

2. 选择排序

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

注意:选择排序相对于冒泡排序只是少了每个相邻元素交换的动作,改为找到最小值下标并交换一次。

2.1 算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  1. 初始状态:无序区为R[1…n],有序区为空;
  2. 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  3. n-1趟结束,数组有序化了。

2.2 代码实现

# 时间复杂度:O(n^2)
# 空间复杂度:O(1)

def select_sort(data):

    length = len(data)

    for i in range(length):
        # 剩余未排序元素中最小元素的下标
        min_index = i
        for j in range(i, length):
            if data[j] < data[min_index]:
                min_index = j
        # 经过一轮循环,找到最小元素下标并交换位置
        data[min_index], data[i] = data[i], data[min_index]

    return data


if __name__ == '__main__':
    data = [3, 2, 1, 5, 6, 4]
    print(select_sort(data))

3. 插入排序

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1 算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。

3.2 代码实现

相对于冒泡和选择排序时间复杂度没有变化,只是算法思想不同。

# 时间复杂度:O(n^2)
# 空间复杂度:O(1)

def insert_sort(data):

    length = len(data)
    for i in range(1, length):
        # 记录当前节点的值
        cur = data[i]
        j = i
        # 倒序遍历,如果前一个数比cur大,则将前一个数后移一位
        while j > 0 and cur < data[j-1]:
            data[j] = data[j-1]
            j -= 1
        # 遍历到比cur小的数时退出循环,将cur赋值给当前下标
        else:
            data[j] = cur

    return data


if __name__ == '__main__':
    data = [3, 2, 1, 5, 6, 4]
    print(insert_sort(data))

4. 希尔排序

希尔排序是插入排序的一种变种。如果数组的最大值刚好是在第一位,使用插入排序要将它挪到正确的位置就需要 n - 1 次移动。也就是说,原数组的一个元素如果距离它正确的位置很远的话,则需要与相邻元素交换很多次才能到达正确的位置,这样是相对比较花时间了。

希尔排序就是为了加快交换速度简单地改进了插入排序,交换不相邻的元素以对数组的局部进行排序。

希尔排序的思想是采用插入排序的方法,先让数组中任意间隔为 h 的元素有序,刚开始 h 的大小可以是 h = n / 2,接着让 h = n / 4,让 h 一直缩小,当 h = 1 时,也就是此时数组中任意间隔为1的元素有序,此时的数组就是有序的了。

4.1 代码实现


def shell_sort(data):
    """
    时间复杂度: O(nlogn)
    空间复杂度: O(n)
    ① 设置间隔从len(num_list)//2开始;每次缩小间隔gap //= 2
    ② 通过gap获取num_list的每个子列表,对子列表采用插入排序
    ③ 核心部分使用插入排序。当gap为1时就是真正的插入排序
    :param data:
    :return:
    """

    length = len(data)
    gap = length // 2
    while gap > 0:
        for start in range(gap):
            insert_sort(data, start, gap)
        gap //= 2
    return data


# 核心部分代码就是插入排序加上间隔gap,当gap为1时就是普通的插入排序
def insert_sort(data, start, gap):
    length = len(data)
    for i in range(start+gap, length, gap):
        # 记录当前节点的值
        cur = data[i]
        j = i
        # 倒序遍历,如果前一个数比cur大,则将前一个数后移一位
        while j > 0 and cur < data[j-1]:
            data[j] = data[j-1]
            j -= 1
        # 遍历到比cur小的数时退出循环,将cur赋值给当前下标
        else:
            data[j] = cur

    return data


if __name__ == '__main__':
    data = [3, 2, 1, 5, 6, 4]
    print(shell_sort(data))

5. 归并排序

归并排序是分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为归并。

5.1 算法描述

  1. 把长度为n的输入序列分成两个长度为n/2的子序列;
  2. 对这两个子序列分别采用递归归并排序;
  3. 将两个排序好的子序列合并成一个最终的排序序列。

5.2 代码实现

# 时间复杂度: O(nlogn)
# 空间复杂度:  O(n)  合并时需要额外的空间保存合并列表
def merge_sort(data):
    # 递归结束条件:只有1个元素,即有序的
    if len(data) <= 1:
        return data
	
	# 拆分为更小的列表,递归调用自身进行拆分、合并
    middle = len(data) // 2
    left = merge_sort(data[:middle])
    right = merge_sort(data[middle:])

	# 合并
    merge = list()
    while left and right:
        if left[0] <= right[0]:
            merge.append(left.pop(0))
        else:
            merge.append(right.pop(0))
    merge.extend(right if right else left)

    return merge


if __name__ == '__main__':
    data = [3, 2, 1, 5, 6, 4]
    print(merge_sort(data))

6. 快速排序

快速排序的基本思想:从数组中选择一个元素,把这个元素称之为中轴元素,然后把数组中所有小于中轴元素的元素放在其左边,所有大于或等于中轴元素的元素放在其右边,显然,此时中轴元素所处的位置的是有序的。也就是说,无需再移动中轴元素的位置。

从中轴元素那里开始把大的数组切割成两个小的数组(两个数组都不包含中轴元素),接着通过递归的方式,让中轴元素左边的数组和右边的数组也重复同样的操作,直到数组的大小为1,此时每个元素都处于有序的位置。

6.1 算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  1. 从数列中挑出一个元素,称为 “基准”(pivot)(一般选择第一个元素);
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.2 代码实现

# 时间复杂度:O(nlogn) 如果总能根据中值把左右两边子列表分成相等的两部分,那么可以达到nlogn;快排的关键是中值选择。
# 空间复杂度:O(1)
def quick_sort(data, start, end):
    if start >= end:
        return data

    # 找到中轴元素下标
    pivot = partition(data, start, end)
    # 递归调用,中轴元素左右两边的子列表
    quick_sort(data, start, pivot-1)
    quick_sort(data, pivot+1, end)

    return data


def partition(data, start, end):
    # 记录中轴元素
    middle_val = data[start]
    left = start + 1
    right = end

    while True:
        # 向右移动找到第一个比middle_val大的下标
        while left <= right and data[left] <= middle_val:
            left += 1
        # 向左移动找到第一个比middle_val小的下标
        while left <= right and data[right] >= middle_val:
            right -= 1

        if left > right:
            break
        # 交换两个元素,使得左边的元素不大于middle_val, 右边的不小于middle_val
        data[left], data[right] = data[right], data[left]

    # 使中轴元素处于有序的位置
    data[start], data[right] = data[right], data[start]

    return right


if __name__ == '__main__':
    data = [3, 2, 1, 5, 6, 4]
    print(quick_sort(data, 0, len(data)-1))

7. 堆排序

参考文章

堆的特点就是堆顶的元素是一个最值,大顶堆的堆顶是最大值,小顶堆则是最小值。

堆排序就是把堆顶的元素与最后一个元素交换,交换之后破坏了堆的特性,再把堆中剩余的元素再次构成一个大顶堆(小顶堆),然后再把堆顶元素与最后第二个元素交换….如此往复下去,等到剩余的元素只有一个的时候,此时的数组就是有序的了。

一般构建堆时:大顶堆—升序、小顶堆—降序。


def heap_sort_asc(lst):
    # 构建大顶堆
    length = len(lst)
    # 从最后一个非叶子节点开始调整,完全二叉树的最后一个非叶子节点是n//2 - 1
    i = length // 2 - 1
    for j in range(i, -1, -1):
        perc_down(lst, j, length-1)

    # 大顶堆顶部元素是最大值,所以和最后一个元素交换,最后的元素是最大值---排序后是升序
    for i in range(length-1):
        lst[0], lst[length-1-i] = lst[length-1-i], lst[0]
        perc_down(lst, 0, length-1-1-i)
    return lst


def heap_sort_desc(lst):
    # 构建小顶堆
    length = len(lst)
    i = length//2-1
    for j in range(i, -1, -1):
        perc_down1(lst, j, length-1)

    # 堆排序---降序
    for i in range(length-1):
        lst[0], lst[length-1-i] = lst[length-1-i], lst[0]
        perc_down1(lst, 0, length-1-1-i)
    return lst


def perc_down(lst, n, length):
    temp = lst[n]
    child = 2*n + 1

    while child <= length:
        # 右节点存在并且值大于左节点,选择右节点下沉
        if child+1 <= length and lst[child+1] > lst[child]:
            child += 1

        # 当前节点大于等于子节点时,下沉完毕
        if lst[child] <= temp:
            break

        lst[n] = lst[child]
        n = child
        child = 2*n + 1

    lst[n] = temp


def perc_down1(lst, n, length):
    temp = lst[n]
    child = 2*n + 1

    while child <= length:
        # 右节点存在并且值小于左节点,选择右节点下沉
        if child+1 <= length and lst[child+1] < lst[child]:
            child += 1

        # 当前节点小于等于子节点时,下沉完毕
        if lst[child] >= temp:
            break

        lst[n] = lst[child]
        n = child
        child = 2*n + 1

    lst[n] = temp


if __name__ == '__main__':
    lst = [6, 5, 1, 2, 3, 4]
    # 升序---构建大顶堆---堆顶元素和最后一个元素交换
    print(heap_sort_asc(lst))
    # 降序---构建小顶堆---堆顶元素和最后一个元素交换
    print(heap_sort_desc(lst))

8. 计数排序

计数排序是一种适合于最大值和最小值的范围不是很大的排序。

**基本思想:**把数组元素作为数组的下标,然后用一个临时数组统计该元素出现的次数,例如 temp[i] = m,表示元素 i 出现了 m 次。最后再把临时数组统计的数据从小到大汇总起来,此时汇总起来是数据是有序的。

# 时间复杂度:O(n+k)
# 空间复杂度:O(k),k是桶个数

def bucket_sort(nums):
    # 1. 找到最值
    min = nums[0]
    max = nums[0]

    for i in nums:
        if i < min:
            min = i
        if i > max:
            max = i

    # 2. max-min+1即数组最大范围,将min作为偏移量
    bucket = [0]*(max - min + 1)

    # 下标代表数字-min,值代表出现的次数
    for i in nums:
        bucket[i-min] = bucket[i-min] + 1

    # 3. 把统计后的数据修改到原数组上
    # 记录下标值
    k = 0
    for index, val in enumerate(bucket):
        # val代表出现了几次,出现几次就添加几个
        for i in range(val):
            # 下标加上min恢复为原数据;
            nums[k] = index + min
            k += 1

    return nums


if __name__ == '__main__':
    nums = [5, 3, 1, 1, 1, 3, 9, 1]
    print(bucket_sort(nums))

LeetCode

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

题目链接

解法1:快速排序

使用快速排序将原列表降序排序后,再找第k个元素。

# 时间复杂度:O(nlogn), 快排理想情况是nlogn,最差是n^2
# 空间复杂度:O(1)

class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        length = len(nums)
        quick_sort(nums, 0, length-1)
        # 降序排序后获取第k-1个元素
        return nums[k-1]


def quick_sort(lst, start, end):
    if start < end:
    	# 获取中轴
        pivot = partition(lst, start, end)
        quick_sort(lst, start, pivot-1)
        quick_sort(lst, pivot+1, end)


def partition(lst, start, end):
    # 将第一个元素作为中间值
    middle_index = start
    left = start + 1
    right = end

    while True:
        # 中间值左侧的元素大于中间值
        while left <= right and lst[left] >= lst[middle_index]:
            left += 1
        # 中间值右侧的元素小于中间值
        while left <= right and lst[right] <= lst[middle_index]:
            right -= 1

        if left > right:
            break
		# 交换不满足条件是值
        lst[left], lst[right] = lst[right], lst[left]
	# 将中间值交换值中轴
    lst[middle_index], lst[right] = lst[right], lst[middle_index]
    return right

解法2:堆排序

  1. 构建大顶堆。
  2. 进行堆排序;因为要获取第k大的元素,所以删除k-1次堆顶元素后堆顶的元素就是第k大的。
class Solution:
    def findKthLargest(self, nums: List[int], k: int) -> int:
        length = len(nums)
        heap_sort(nums, length, k)
        return nums[0]


def heap_sort(lst, length, k):
    # 1. 构建大顶堆
    # 获取最后一个非叶子节点
    i = length//2-1
    for j in range(i, -1, -1):
        site_down(lst, j, length-1)

    # 2. 堆排序---获取第k大的元素,即删除k-1次顶部元素后顶部的元素就是第k大。
    for i in range(k-1):
        lst[0], lst[length-1-i] = lst[length-1-i], lst[0]
        site_down(lst, 0, length-1-1-i)


def site_down(lst, n, length):
    temp = lst[n]
    # 左子节点
    child = 2*n+1
    while child <= length:
        # 选择左右子节点中更小的节点
        if child+1 <= length and lst[child+1] > lst[child]:
            child += 1

        # 子节点都大于temp时停止下沉
        if temp >= lst[child]:
            break

        lst[n] = lst[child]
        n = child
        child = 2*n + 1

    lst[n] = temp

347. 前 K 个高频元素

题目链接

解法1:桶排序

# 时间复杂度:O(n)
# 空间复杂度:O(k),存放mapping和bucket的空间
def topKFrequent(nums, k):

    # 1.获取i和i个数的映射
    mapping = {}
    for i in nums:
        mapping[i] = mapping.get(i, 0) + 1

    # 2. 获取次数和元素映射;  找到出现次数的最小值和最大值;
    # 获取最大值和最小值是为了减小建桶空间的占用
    min_val = 0
    max_val = 0
    count_num_map = {}
    for key, value in mapping.items():
        if value < min_val:
            min_val = value
        elif value > max_val:
            max_val = value
        temp = count_num_map.get(value, [])
        temp.append(key)
        count_num_map[value] = temp

    # 3. 建桶,桶下标代表出现出现的次数,桶内放出现的元素
    bucket = [None] * (max_val-min_val+1)
    for index, num in enumerate(bucket):
        if index in count_num_map:
            bucket[index] = count_num_map[index]

    # 4. 倒序遍历桶获取k个元素
    result = []
    for i in range(len(bucket)-1, 0, -1):
        if len(result) < k and bucket[i]:
            result.extend(bucket[i])

    return result

if __name__ == '__main__':
    nums = [1, 1, 1, 2, 2, 3]
    k = 2
    print(topKFrequent(nums, k))

解法2:堆排序

  1. 获取每个元素和元素的个数映射关系: {“key1”: “key1个数”, “key2”: “key2个数”…};
  2. 根据每个元素的个数构建小顶堆。
def topKFrequent(nums, k):
    mapping = {}
    # 1. 获取 i 和 i 个数的映射
    for i in nums:
        mapping[i] = mapping.get(i, 0) + 1

    return heap_sort(mapping, k)


def heap_sort(mapping, k):
    # 2. 构建小顶堆
    result = []

    for key, val in mapping.items():
        # 如果当前堆的元素个数小于k,则直接加入堆顶并执行下沉
        if len(result) < k:
            result.insert(0, key)
            site_down(result, 0, len(result)-1, mapping)
        # 如果当前堆得元素个数等于k,则比较当前key和堆顶元素的个数大小,如果大于堆顶元素个数则替换到堆顶并执行下沉。
        else:
            if val > mapping[result[0]]:
                result[0] = key
                site_down(result, 0, len(result)-1, mapping)
    return result


def site_down(result, n, length, mapping):
    temp = result[n]
    # 默认选择左子节点
    child = 2*n+1
    while child <= length:
        # 选择左右子节点中更小的节点进行下沉
        if child+1 <= length and mapping[result[child+1]] < mapping[result[child]]:
            child += 1

        # 左右子节点都大于等于temp时停止下沉
        if mapping[temp] <= mapping[result[child]]:
            break

        # 子节点上升到当前节点
        result[n] = result[child]
        n = child
        child = 2*n + 1

    # 最后将当前节点交换到子节点
    result[n] = temp


if __name__ == '__main__':
    nums = [1, 1, 1, 2, 2, 3]
    k = 2
    print(topKFrequent(nums, k))

451. 根据字符出现频率排序

题目链接

解法1:桶排序

def topKFrequent(s):

    # 1.获取i和i个数的映射
    mapping = {}
    for i in s:
        mapping[i] = mapping.get(i, 0) + 1

    # 2. 获取次数和元素映射;  找到出现次数的最小值和最大值;
    min_val = 0
    max_val = 0
    count_num_map = {}
    for key, value in mapping.items():
        if value < min_val:
            min_val = value
        elif value > max_val:
            max_val = value
        temp = count_num_map.get(value, [])
        temp.append(key)
        count_num_map[value] = temp

    # 3. 建桶,桶下标代表出现出现的次数,桶内放出现的元素
    bucket = [[]] * (max_val-min_val+1)
    for index, num in enumerate(bucket):
        if index in count_num_map:
            bucket[index] = count_num_map[index]

    # 4. 倒序遍历桶获取k个元素
    result = []
    for i in range(len(bucket)-1, 0, -1):
        for item in bucket[i]:
            for j in range(i):
                result.extend(item)

    return "".join(result)


if __name__ == '__main__':
    s = "tree"
    print(topKFrequent(s))

75. 颜色分类

题目链接

解法1

def sortColors(nums):

    mapping = {}
    for i in nums:
        mapping[i] = mapping.get(i, 0) + 1

    index = 0
    for i in range(3):
        if index >= len(nums):
            break
        for j in range(mapping.get(i, 0)):
            nums[index] = i
            index += 1
    return nums


if __name__ == '__main__':
    nums = [2,0,2,1,1,0]
    print(sortColors(nums))

解法2:堆排序

直接使用堆排序

# 时间复杂度:O(nlogn)
# 空间复杂度:O(1)
def sortColors(nums):

    return heap_sort(nums)


def heap_sort(nums):
    # 1. 构建大顶堆
    # 最后一个非叶子节点
    length = len(nums)
    for i in range(length//2-1, -1, -1):
        site_down(nums, i, length-1)

    # 2. 排序
    for i in range(length-1):
        nums[0], nums[length-i-1] = nums[length-i-1], nums[0]
        site_down(nums, 0, length-i-1-1)

    return nums


def site_down(nums, n, length):
    temp = nums[n]
    # 默认选择左子节点
    child = 2*n+1
    while child <= length:
        # 选择左右子节点中更大的节点进行下沉
        if child+1 <= length and nums[child+1] > nums[child]:
            child += 1

        # 左右子节点都小于等于temp时停止下沉
        if temp >= nums[child]:
            break

        # 子节点上升到当前节点
        nums[n] = nums[child]
        n = child
        child = 2*n + 1

    # 最后将当前节点交换到子节点
    nums[n] = temp


if __name__ == '__main__':
    nums = [2, 0, 2, 1, 1, 0]
    print(sortColors(nums))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值