首页 > 技术文章 > 算法归纳1-排序(模板)

tensorzhang 2021-10-07 23:49 原文

1,题目

leetcode-912. 排序数组

2,选择排序

  • 核心思想:挑出最值放在一边
  • 时间复杂度:O(N^2)
  • 示例代码:
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        length = len(nums)
        for i in range(length):
            minIndex = i
            for j in range(i+1, length):
                if nums[minIndex] > nums[j]:
                    minIndex = j
            middle = nums[i]
            nums[i] = nums[minIndex]
            nums[minIndex] = middle
        return nums

3,冒泡排序

  • 核心思想:通过交换两个相邻元素将最值逐渐依次移向一边
  • 时间复杂度:O(N^2)
  • 示例代码:
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        length = len(nums)
        for i in range(length):
            for j in range(1, length-i):
                if nums[j-1] > nums[j]:
                    middle = nums[j-1]
                    nums[j-1] = nums[j]
                    nums[j] = middle
        return nums

4,插入排序

  • 核心思想:排列好前N个元素,第N+1个元素插入到前N个元素中的适当位置
  • 时间复杂度:O(N)~O(N^2)
  • 示例代码:
def insert(nums, i, j):
    #将j插入到i处(j>i),这意味着首先保存j处的数字,并将i及之后的数字全部后移,再将保存的数字放到原i处
    middle = nums[j]
    for index in range(j-1, i-1, -1):
        nums[index+1] = nums[index]
    nums[i] = middle

class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        length = len(nums)
        for i in range(1, length):
            if nums[i] < nums[i-1]:
                #此时需要进行插入排序,边界条件1
                if nums[i] < nums[0]:
                    #直接插入开头,边界条件2
                    insert(nums, 0, i)
                else:
                    for j in range(1, i):
                        if nums[i]>=nums[j-1] and nums[i]<nums[j]:
                            insert(nums, j, i)
        return nums

5,归并排序

  • 核心思想:分而治之,并在分治过程中保留各部分序列信息
  • 时间复杂度:O(NlogN)
  • 额外空间复杂度:O(NlogN)
  • 示例代码:
class Solution {
    public int[] sortArray(int[] nums) {
        return mergeSort(nums, 0, nums.length);  //左闭右开区间
    }

    int[] mergeSort(int[] nums, int i, int j){
        if(i==j-1){
            //左闭右开区间对应的结束条件
            return new int[]{nums[i]};
        }
        int middle = (i+j)>>1;  //左闭右开区间对应的middle计算方法
        int[] left = mergeSort(nums, i, middle);
        int[] right = mergeSort(nums, middle, j);
        return merge(left, right);
    }

    int[] merge(int[] nums1, int[] nums2){
        int length1 = nums1.length;
        int length2 = nums2.length;
        int[] res = new int[length1+length2];
        int i = 0;
        int j = 0;
        while(i<length1 && j<length2){
            if(nums1[i]<=nums2[j]){
                res[i+j] = nums1[i];
                i++;
            }else{
                res[i+j] = nums2[j];
                j++;
            }
        }
        while(i<length1){
            res[i+j] = nums1[i];
            i++;
        }
        while(j<length2){
            res[i+j] = nums2[j];
            j++;
        }

        return res;
    }
}

5.1,归并排序衍生求解:剑指 Offer 51. 数组中的逆序对

class Solution:
    def __init__(self):
        self.reversePairNum = 0

    def reversePairs(self, nums: List[int]) -> int:
        if not nums:
            return 0

        def merge(nums1, nums2):
            res = []
            index1 = 0
            length1 = len(nums1)
            index2 = 0
            length2 = len(nums2)
            while index1<length1 and index2<length2:
                if nums1[index1] <= nums2[index2]:
                    res.append(nums1[index1])
                    index1+=1
                else:
                    res.append(nums2[index2])
                    index2+=1
                    #统计左侧数组中大于右侧数组对应位置元素的元素的个数
                    self.reversePairNum += length1 - index1
            while index1<length1:
                res.append(nums1[index1])
                index1+=1
            while index2<length2:
                res.append(nums2[index2])
                index2+=1
            return res

        def mergeSort(nums, i, j):
            if i==j:
                return [nums[i]]
            middle = i+((j-i)>>1)
            numsLeft = mergeSort(nums, i, middle)
            numsRight = mergeSort(nums, middle+1, j)
            return merge(numsLeft, numsRight)

        mergeSort(nums, 0, len(nums)-1)
        return self.reversePairNum

5.2,数组小和、单调和

6,快速排序

  • 核心思想:

    子问题:荷兰国旗问题

    • 如何将一个数组nums分为小于某个数num,等于num,大于num的三段
      • 解法1:准备三个空数组,遍历一遍原数组,然后将三个数组合并
      • 解法2:左指针i指向原数组最左侧代表小于数组的右边界(i=0),右指针j指向原数组最右侧代表大于数组的左边界(j=len(nums)-1),利用指针k从左向右遍历数组,有以下三种情况:
        • 如果nums[k]<num,交换nums[k]与nums[i],i、k均加一
        • 如果nums[k]==num,k加一
        • 如果nums[k]>num,交换nums[k]与nums[j],j减一
      • 示例代码
        def exchange(nums, i, j):
            middle = nums[i]
            nums[i] = nums[j]
            nums[j] = middle
        
        def middleFunction(nums, num):
            i = 0                   #小数组左边界
            j = len(nums)-1         #大数组右边界    
            k = 0                   #当前数
            while k<=j:
                if nums[k] < num:
                    #将nums[k]与nums[i]互换位置,并且i、k均+1(扩大小数组的边界)
                    exchange(nums, i, k)
                    i+=1
                    k+=1
                elif nums[k] == num:
                    #k+1,其余不变
                    k+=1
                else:
                    #将nums[k]与nums[j]互换位置,并且j-1(扩大大数组边界,并且由于交换后的数据没有处理过因此k不变)
                    exchange(nums, j, k)
                    j-=1
        
  • 快速排序:首先在nums随机选取一个数据并于最右侧的数交换,以最右侧的数做为num进行三段划分,然后分别对小数组和大数组递归调用该过程。
    • 时间复杂度:O(NlogN)
    • 额外空间复杂度:O(logN)
    • 示例代码:
    class Solution {
        public int[] sortArray(int[] nums) {
            quickSort(nums, 0, nums.length-1, nums[nums.length-1]);
            return nums;
        }
    
        void quickSort(int[] nums, int i, int j, int k){
            if(i>=j){
                return;
            }
    
            int iCached = i;
            int jCached = j;
            int presIndex = i;
            while(presIndex<=j){
                if(nums[presIndex]<k){
                    exchange(nums, presIndex, i);
                    presIndex++;
                    i++;
                }else if(nums[presIndex]==k){
                    presIndex++;
                }else{
                    exchange(nums, presIndex, j);
                    j--;
                }
            }
    
            quickSort(nums, iCached, i-1, nums[iCached]);
            quickSort(nums, j+1, jCached, nums[jCached]);
        }
    
        void exchange(int[] nums, int i, int j){
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
        }
    }
    
  • 变化用法-求第k小/大的数字:
    • https://blog.csdn.net/qiaoxinwei/article/details/108976111
    • 算法思想:基于快排的思想,从数组a[]中找出一个基准值v,把数组分为两部分a[l...j]和a[j+1...r]。a[l...j]中的元素小于v,a[j+1...r]中元素大于v。这时有两种情况:
      • a[l...j]中元素的个数大于等于k,则递归到数组a[l...j]中搜索的第k小的数。
      • a[l...j]中元素的个数小于k,则递归到数组a[j+1...r]中第k-(j-l+1)小的数。
    • 时间复杂度:因为每次分区完只需要继续操作一边,所以该算法的平均时间复杂度是O(n)。
    • 例子(只是会超出内存限制):668. 乘法表中第k小的数

7,堆排序-java.util.PriorityQueue

  • 堆的基础知识:
    • 连续数组与完全二叉树的对应关系:数组下标为i,左孩子对应下标2*i+1,右孩子对应下标2*i+2,父位置对应下标(i-1)/2
      • 堆是一种特殊的完全二叉树,分为大根堆和小根堆
      • 大根堆:每一棵子树的最大值都是头节点
      • 小根堆:每一棵子树的最小值都是头节点
    • 基础操作一:大根堆和小根堆的转换(插入一个值并保持大/小根堆-heapinsert,时间复杂度:logN):
      • 将数组转换为大根堆:每插入一个数就和父节点对比,如果比父节点大就进行交换,并再和父节点对比;如果小了就停止。(使用heapsize记录堆的大小)
      • 将数组转换为小根堆:类似。
    • 基础操作二:返回并删除大根堆中的最大值(heapify,时间复杂度:logN):
      • 返回最大值:返回头节点。
      • 删除最大值:堆范围内数组的最后一值放到头节点arr[0]=arr[heapsize-1];heapsize=heapsize-1;左右子节点选出最大值与父节点对比,如果大就替换并再比较交换后的数字与其左右孩子,不大就终止,从上到下执行此过程。
      • 返回并删除小根堆中的最小值与上述操作类似。
    • 组合操作:如果在大根堆上任意一个位置将数字进行了改变后还要保持大根堆,可以对比数字是变大了还是变小了;如果是变大了,就向上进行一个heapinsert操作;如果变小了,就向下进行一个heapify操作。(或者无脑heapinsert,heapify,能调整会自动调整,不能调整就保持原样不动)
  • 堆排序:
    • 算法步骤:
      • 先将数组转化为大根堆(heapsize从零逐渐+1,执行heapinsert操作)
      • 将堆数组首尾交换,heapsize-1(堆数组最大值此时在堆数组最后,并且和堆断开联系)
      • 堆从首到尾执行heapify,并重复步骤2,3
    • 时间复杂度:O(NlogN)
    • 额外空间复杂度:O(1)
class Solution:
    def sortArray(self, nums: List[int]) -> List[int]:
        '''基于大根堆进行排序'''
        self.nums = nums
        self.heapNum = 0

        def heapInsert(index):
            '''新进来一个数保持大根堆'''
            while (index > 0) and (self.nums[index] > self.nums[(index-1)//2]):
                middle = self.nums[index]
                self.nums[index] = self.nums[(index-1)//2]
                self.nums[(index-1)//2] = middle
                index = (index-1)//2

        def heapIfy(index):
            '''首元素替换后保持大根堆'''
            while (2*index+1) < self.heapNum:
                if (2*index+2) < self.heapNum:
                    if self.nums[2*index+1] > self.nums[2*index+2]:
                        maxChildrenIndex = 2*index+1
                        maxChildren = self.nums[2*index+1]
                    else:
                        maxChildrenIndex = 2*index+2
                        maxChildren = self.nums[2*index+2]
                else:
                    maxChildrenIndex = 2*index+1
                    maxChildren = self.nums[2*index+1]

                if maxChildren > self.nums[index]:
                    middle = self.nums[index]
                    self.nums[index] = self.nums[maxChildrenIndex]
                    self.nums[maxChildrenIndex] = middle
                    index = maxChildrenIndex
                else:
                    break

        '''排序操作'''
        for i in range(len(self.nums)):
            #首先将数据集转化为大根堆
            heapInsert(i)
            self.heapNum += 1
        while self.heapNum > 0:
            #排序
            middle = self.nums[0]
            self.nums[0] = self.nums[self.heapNum-1]
            self.nums[self.heapNum-1] = middle
            self.heapNum -= 1
            heapIfy(0)

        return self.nums

8,桶排序

  • 先映射到多个桶中做到基本有序,再在每个桶中进行比较排序
    桶排序和基数排序
  • 比如直到所有的数肯定都在[1,10]的区间内,那么就可以遍历一遍对[1,10]内的数字进行计数,然后再依据计数结果分别将数字输出就是排序后的结果。

9,总结

算法 时间复杂度 空间复杂度 稳定性
选择 O(N^2) O(1) 不稳定
冒泡 O(N^2) O(1) 稳定
插入 O(N^2) O(1) 稳定
归并 O(NlogN) O(N) 稳定
快排(随机) O(NlogN) O(NlogN) 不稳定
堆排序 O(NlogN) O(1) 不稳定
  • 稳定性的价值体现在按照不同的标准多次排序时,后面的排序不会打破之前排序带来的有序性。(局部的有序)
  • 基于比较的排序目前还没有时间复杂度低于 O(NlogN) 的算法
  • 目前没有时间复杂度为 O(NlogN),空间复杂度低于 O(N) 且 保持稳定 的算法
  • 归并排序的额外空间复杂度可以变成O(1),但是很难,可以看“归并排序 内部缓存法”
  • “原地归并排序”会让时间复杂度变成O(N^2)
  • 快速排序可以做到稳定,但是很难,可以看“O1 stable sort”
  • 一般用随即快排,因为实验表明最快(常数时间低);如果需要稳定会用归并排序。
  • 一般语言中的算法会根据各排序算法的优势编写很复杂的排序算法策略,为了综合使用各排序算法各自的优势。(比如快排当中间部分数据很少时,对该序列直接用插入排序就返回,这里就是利用大量数据时快排的调度优势,和少量数据量时插入排序常数时间低的优势)
  • 系统实现的排序对基础类型使用随机快排,而对自定义类型用归并。
    • 因为基础类型肯定不需要稳定性,所以用常数时间低的快排;而如果是自定义数据类型,由于无法确定是否需要稳定性,所以会用归并排序保持稳定性。

10,外部排序

  • 有时,待排序的文件很大,计算机内存不能容纳整个文件,这时候对文件就不能单纯的使用内部排序了,还得采取一些其他的策略。最通常的做法是对待排序的大文件进行分割,变成一个个很小的、内存可以容纳的数据片段,然后对这些分片分别排序,最后通过归并算法将这些片段合并,变为有序。(分割排序+归并)
    • 采用适当的内部排序方法对输入文件的每个片段进行排序,将排好序的片段(成为归并段)写到外部存储器中(通常由一个可用的磁盘作为临时缓冲区),这样临时缓冲区中的每个归并段的内容是有序的。
    • 利用归并算法,归并第一阶段生成的归并段,直到只剩下一个归并段为止。
  • 外部排序

推荐阅读