算法&数据结构(二):查找与排序算法

重点掌握二分查找、归并排序、快速排序

不同排序算法的时间复杂度与空间复杂度

1. 冒泡排序

比较相邻的元素,如果第一个比第二个大,就交换。从开始到结尾,这步做完后,最后的元素会是最大的数。然后重复,从开始元素到倒数第二。

a = [1,5,6,0,2,4,1,2,0,5,3]

for i in range(len(a)):
    # 设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成。
    flag = True
    for j in range(len(a)-i-1):
        if a[j] > a[j+1]:
            a[j], a[j+1] = a[j+1], a[j]
            flag = False
    if flag:
        break
print(a)

2. 选择排序

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。

a = [1,5,6,0,8,22,2,4,1,2,0,5,3]

# 总共要经过 N-1 轮比较
for i in range(len(a)-1):
    # 每次重新定义最小值的下标
    min_v = i
    # 每轮需要比较的次数 N-i
    for j in range(i+1, len(a)):
        if a[j] < a[min_v]:
            # 记录目前能找到的最小值元素的下标
            min_v = j
    # 最小值交换
    if i != min_v:
        a[min_v], a[i] = a[i], a[min_v]

print(a)

3. 插入排序

从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。

a = [1,5,6,0,8,22,2,4,1,2,0,5,3]

for i in range(len(a)-1):
    if a[i] > a[i+1]:
        for j in range(i,-1,-1):
            if a[j] > a[j+1]:
                a[j], a[j+1] = a[j+1], a[j]

print(a)

4. 归并排序

分割:先设定两个指针start和end,分别为序列的起始位置。求出中间指针,按照中间位置进行递归拆分,分到最细之后对两个有序数列合并。

合并:设定两个指针i和j,比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置,到达某一方指针的尾部后,将另一序列剩下的所有元素直接复制到合并序列尾。

# 将两个有序数列合并
def merge_list(a, l, mid, r):
    temp = len(a)*[0]
    i = l
    j = mid+1
    k = 0
    # 将左右两个数列合并
    while i <= mid and j <= r:
        if a[i] <= a[j]:
            temp[k] = a[i]
            i += 1
            k += 1
        else:
            temp[k] = a[j]
            j += 1
            k += 1
    # 剩余数列值的添加
    while i <= mid:
        temp[k] = a[i]
        i += 1
        k += 1
    # 剩余数列值的添加
    while j <= r:
        temp[k] = a[j]
        j += 1
        k += 1
    # 返回到a
    for i in range(k):
        a[l+i] = temp[i]


def merge_sort(c, start, end):
    # mid 如果缺少限制条件,会不断被计算
    if start < end:
        m = (start + end) // 2
        # 左边有序
        merge_sort(c, start, m)
        # 右边有序
        merge_sort(c, m+1, end)
        # 将二个有序数列合并
        merge_list(c, start, m, end)


xin = [1,5,6,0,8,22,2,4,1,2,0,5,3,32,23,4,-1,3]

merge_sort(xin, 0, len(xin)-1)

5. 快速排序

从数列中挑出一个元素,称为 “基准”(pivot),比基准值小的在左边,比基准值大的在右边,基准在中间位置。递归排序左右子分区,不包含基准本身。

具体实现:设置基准为最右边的元素,初始化一个基准指针,位于区间最左边的位置-1。如果当前元素比基准值小, 基准指针+1,交换当前元素和基准指针对应的元素。基准指针(排序后的位置+1)指向基准的位置,再次交换基准指针对应位置与最右边的元素

与归并排序的区别在于,快速是定基准分区,再在分区里面递归。而归并是先分为左右序列,再合并序列。

import random


# 把数组分为两个部分,比基准值小的摆放在左边,比基准值大的摆在基准的右边。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作
def partition(data, l, r):
    # 根据区间大小产生随机数,作为基准
    p = random.randint(l, r)
    # 将基准放在最右边
    data[r], data[p] = data[p], data[r]
    # 设置一个基准指针,位于区间最左边的位置-1,每摆放一个比基准值小的值,就移动一步
    p_position = l - 1

    # 所有元素比基准值小的摆放在基准的左边
    # 注意!!!此处范围应该是两个指针之间,而不是data的长度!
    for i in range(l, r):
        if data[i] < data[r]:
            p_position += 1
            if p_position != i:
                data[p_position], data[i] = data[i], data[p_position]
    # 基准指针(排序后的位置+1)就是基准的位置
    p_position += 1
    data[p_position], data[r] = data[r], data[p_position]

    return p_position


def quick_sort(c, start, end):
    if c == [] or start < 0 or start >= end:
        return c

    if start < end:
        pi = partition(c, start, end)
        quick_sort(c, start, pi-1)
        quick_sort(c, pi + 1, end)

a = [1,5,6,0,8,22,2,4,1,2,0,5,3,32,23,4,-1,3]

quick_sort(a, 0, len(a)-1)
print(a)

6. 桶排序

设置固定数量的空桶,把数据放到对应的桶中,对每个不为空的桶中数据进行排序,拼接不为空的桶中数据,得到结果。

或者设置(最大值-最小值+1)数量的空桶,把数据放到对应的桶中,拼接不为空的桶中数据,得到结果。


def bucket_sort(c):
    buckets = [0] * (max(c) - min(c) + 1)
    for i in c:
        buckets[i-min(c)] += 1
    res = []
    for i in range(len(buckets)):
        if buckets[i] != 0:
            # buckets[i]为次数
            res.extend([i + min(c)] * buckets[i])
    print(res)


a = [2,6,8,10,12,2,4,8,22,8,8]
bucket_sort(a)

leetcode:153. 寻找旋转排序数组中的最小值

问题描述:假设按照升序排序的数组在预先未知的某个点上进行了旋转。请找出其中最小的元素。你可以假设数组中不存在重复元素。

解法:最小元素正好是两个子数组的分界线,可以用二分查找法

时间复杂度:O(logn)

class Solution:
    def findMin(self, nums: List[int]) -> int:
        if len(nums) <= 0:
            return 0
        p1 = 0
        p2 = len(nums) - 1
        # 没有旋转
        if nums[p1] < nums[p2]:
            return nums[p1]
        while nums[p1] >= nums[p2]:
            if p1 == p2 - 1:
                return nums[p2]
            mid = (p1 + p2) // 2
            # mid 为左子数组的递增
            if nums[mid] >= nums[p1]:
                p1 = mid
            # mid 为右子数组的递增
            elif nums[mid] <= nums[p2]:
                p2 = mid

leetcode:154. 寻找旋转排序数组中的最小值 II

问题描述:假设按照升序排序的数组在预先未知的某个点上进行了旋转。请找出其中最小的元素。注意数组中可能存在重复的元素。

解法:例如{10111}和{11101},p1=p2=mid,无法判断中间的数字位于前面还是后面的递增数组,只能使用顺序查找

时间复杂度:O(logn)或者O(n)

class Solution:
    def findMinOrder(self, arr, index1, index2):
        res = arr[index1]
        for i in range(index1, index2):
            if arr[i] < res:
                res = arr[i]
        return res
    
    def findMin(self, nums: List[int]) -> int:
        if len(nums) <= 0:
            return 0
        p1 = 0
        p2 = len(nums) - 1
        # 没有旋转
        if nums[p1] < nums[p2]:
            return nums[p1]
        # 如果rotateArray[p1] < rotateArray[p2] 说明是递增数列
        while nums[p1] >= nums[p2]:
            if p1 == p2 - 1:
                return nums[p2]
            mid = (p1 + p2) // 2
            # 如果p1、p2、mid指向的三个数相等,则只能顺序查找
            if nums[mid] == nums[p1] and nums[mid] == nums[p2]:
                return self.findMinOrder(nums, p1, p2)
            # mid 为左子数组的递增
            if nums[mid] >= nums[p1]:
                p1 = mid
            # mid 为右子数组的递增
            elif nums[mid] <= nums[p2]:
                p2 = mid
        

剑指offer:旋转数组的最小数字

问题描述:把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。 输入一个非减排序的数组的一个旋转,输出旋转数组的最小元素。 例如数组{3,4,5,1,2}为{1,2,3,4,5}的一个旋转,该数组的最小值为1。 NOTE:给出的所有元素都大于0,若数组大小为0,请返回0。

解法:最小元素正好是两个子数组的分界线,可以用二分查找法

时间复杂度:O(logn)

# -*- coding:utf-8 -*-
class Solution:
    def minNumberInRotateArray(self, rotateArray):
        if len(rotateArray) <= 0:
            return 0
        p1 = 0
        p2 = len(rotateArray) - 1
        # 没有旋转
        if rotateArray[p1] < rotateArray[p2]:
            return rotateArray[p1]
        # 如果rotateArray[p1] < rotateArray[p2] 说明是递增数列
        while rotateArray[p1] >= rotateArray[p2]:
            if p1 == p2 - 1:
                return rotateArray[p2]
            mid = (p1 + p2) // 2
            if rotateArray[mid] == rotateArray[p1] and rotateArray[mid] == rotateArray[p2]:
                res = rotateArray[p1]
                for i in range(p1, p2+1):
                    if rotateArray[i] < res:
                        res = rotateArray[i]
                return res
            # mid 为左子数组的递增
            if rotateArray[mid] >= rotateArray[p1]:
                p1 = mid
            # mid 为右子数组的递增
            elif rotateArray[mid] <= rotateArray[p2]:
                p2 = mid
        
        

剑指offer:数组中的逆序对

问题描述:在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。 即输出P%1000000007

解法:归并排序。左子数组的逆序对 + 右子数组的逆序对 + 合并的逆序对,合并的时候把较大的数字从后往前复制到辅助数组中,辅助数组的元素递增

时间复杂度:O(nlogn)

class Solution:
    def merge_sort(self, data, copy, start, end):
        if start == end:
            copy[start] = data[start]
            return 0
        mid = (end - start) // 2
        
        # 拆分成两个数组
        # copy, data 交换
        left = self.merge_sort(copy, data, start, start + mid)
        right = self.merge_sort(copy, data, start + mid + 1, end)

        # 两个子数组的合并
        count = 0
        # 前一个数组的末尾下标
        i = start + mid
        # 后一个数组的末尾下标
        j = end
        # 下标:辅助数组从后往前添加较大的元素
        t_index = end
        while i >= start and j >= (start + mid + 1):
            # 左比右大,说明产生多个逆序对,个数为右边余下的数字个数
            if data[i] > data[j]:
                copy[t_index] = data[i]
                count += (j - start - mid)
                i -= 1
                t_index -= 1
            # 右比左大,没有逆序对
            else:
                copy[t_index] = data[j]
                j -= 1
                t_index -= 1
        # 左数组有剩余
        while i >= start:
            copy[t_index] = data[i]
            t_index -= 1
            i -= 1
        # 右数组有剩余
        while j >= (start + mid + 1):
            copy[t_index] = data[j]
            t_index -= 1
            j -= 1
            
        # print(copy == data)
        # print('data = ', data)
        # print('copy = ', copy)
        # 左子数组的逆序对 + 右子数组的逆序对 + 合并的逆序对
        return left + right + count

    def InversePairs(self, data):
        if not data:
            return 0
        # 辅助数组的元素递增
        copy = [i for i in data]
        # 调用递归
        count = self.merge_sort(data, copy, 0, len(data) - 1)
        return count % 1000000007

leetcode:35. 搜索插入位置

问题描述:给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

解法:二分查找

时间复杂度:O(logn)

class Solution(object):
    def searchInsert(self, nums, target):
        if not nums:
            return 0
        left = 0
        # 如果target比nums里所有的数都大,则最后一个数的索引 + 1 就是候选值,因此,右边界应该是数组的长度
        right = len(nums)
        # 退出循环时,left == right
        while left < right:
            # 选左中位数,防止 left + right 导致整型溢出
            mid = (left + right) >> 1
            if nums[mid] < target:
                left = mid + 1
            else:
                right = mid
        return left
                

leetcode:378. 有序矩阵中第K小的元素

问题描述:给定一个n x n矩阵,其中每行和每列元素均按升序排序,找到矩阵中第k小的元素。注意,它是排序后的第k小元素。

解法:二分查找法,根据题目可得左上角元素最小,右下角元素最大,计算中间值。然后计算小于等于目标值的元素个数,根据递增规则,从右上角开始查找,类似于题目“二维数组的查找”

时间复杂度:O(nlogk) ,k=最大值-最小值

class Solution(object):
    def kthSmallest(self, matrix, k):
        # 计算小于等于目标值的元素个数,根据递增规则,从右上角开始查找
        def count_num(m, target):
            i = 0
            j = len(m) - 1
            ans = 0
            while i < len(m) and j >= 0:
                if m[i][j] <= target:
                    ans += j + 1
                    i += 1
                else:
                    j -= 1
            return ans
        
        #  思路:左上角元素最小,右下角元素最大,计算小于等于中间值的元素个数
        left = matrix[0][0]
        right = matrix[-1][-1]
        # 二分法查找
        while left < right:
            mid = (left + right) >> 1
            # print(' mid = ', mid)
            count = count_num(matrix, mid)
            # print('count = ', count)
            if count < k:
                left = mid + 1
            else:
                right = mid
        return left

        
发布了93 篇原创文章 · 获赞 119 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_18310041/article/details/95196624