算法刷题总结(五)排序

常见的排序算法

在这里插入图片描述

下面介绍各排序算法的思路和代码,其中快速排序和归并排序的代码可以在 leetcode. 912 排序数组 里进行测试。

快速排序(QuickSort)

快速排序从数组中随机挑一个数(叫做pivot),把比它小的数放到它左侧,把比它大的数放到它右侧,再对它左侧和右侧的子数组分别重复这个操作。

快排分三步:(1)停止条件;(2)partition函数找到pivot应该在的位置;(3)对pivot左右part分别递归。快排最关键的是partition函数,它将pivot放到该在的位置并返回这个位置。partition函数有lomuto、hoare、经典快排三种实现方式,下面分别给出这三种实现。

1. lomuto partition

把随机选出的pivot放到末尾,用j往右遍历整个数组,遍历过程中将比pivot小的元素换到前面,用i来标记已知的小元素在的区间[1, i)。等j遍历完,位置i上的元素就是第一个>=pivot的元素,把pivot换到这个位置上去。
优点:不易出错,单向遍历(适合链表的快排)

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        if(nums.empty()) return nums;
        quickSort(nums, 0, nums.size() - 1); //左闭右闭的写法,左右区间都能取到
        return nums;
    }

    void quickSort(vector<int>& nums, int low, int high)
    {
        if(high <= low) return; //迭代终止条件:没有元素(<)或只有一个元素(=)
        int p = partition(nums, low, high);
        quickSort(nums, low, p - 1);
        quickSort(nums, p + 1, high);
    }

    int partition(vector<int> &nums, int low, int high)
    {
        //partition的Lomuto实现方式
        int pivot = low + rand() % (high - low + 1); //随机选取一个数字作为pivot。也可以直接取开头或结尾的数字,但遇到不好的case会超时
        swap(nums[pivot], nums[high]); //把pivot放在结尾,会好写一点
        int i = low;
        for(int j = i; j < high; ++j)
        {
            if(nums[j] < nums[high])
            {
                swap(nums[i], nums[j]);
                ++i;
            }
        }
        swap(nums[i], nums[high]); // [1, i)是所有小于pivot的元素,pivot在结尾,把i位置的元素跟pivot换一下就行了
        return i;
    }
};
2. hoare partition

把中间的元素当做pivot,用i和j分别从左侧和右侧向中间遍历数组,i记录比pivot小的,j记录比pivot大的,如果不满足条件就交换i和j的元素。注意pivot可能会被换位置,返回的j只能保证pivot在[low, j]区间内,但不一定在位置j上,所以迭代时要取到p。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        if(nums.empty()) return nums;
        quickSort(nums, 0, nums.size() - 1); //左闭右闭的写法,左右区间都能取到
        return nums;
    }

    void quickSort(vector<int>& nums, int low, int high)
    {
        if(high <= low) return; //迭代终止条件:没有元素(<)或只有一个元素(=)
        int p = partition(nums, low, high);
        quickSort(nums, low, p); //hoare partition时,locPivot位置并非一定是pivot所在的位置,pivot在[low, p]内任一位置,所以这里取到了p、不是取到p-1
        quickSort(nums, p + 1, high);
    }

    int partition(vector<int> &nums, int low, int high)
    {
        //partition的hoare实现方式
        int p = low + (high - low) / 2;
        int pivot = nums[p]; //hoare partition时,一开始选的pivot可能会被换位置,所以要跟pivot的值比较,不能跟nums这个位置上的的数字比较
        int i = low, j = high;
        while(true)
        {
            while(nums[i] < pivot) ++i;
            while(nums[j] > pivot) --j;
            if(i >= j) return j;
            swap(nums[i], nums[j]);
            ++i;
            --j;
        }
    }
};
3. 经典快排

对hoare partition的改进,减少交换次数。

class Solution {
public:
    vector<int> sortArray(vector<int>& nums) {
        if(nums.empty()) return nums;
        quickSort(nums, 0, nums.size() - 1); //左闭右闭的写法,左右区间都能取到
        return nums;
    }

   void quickSort(vector<int> & nums, int low, int high)
    {
        if(low >= high) return;
        int locPivot = partition(nums, low, high);
        quickSort(nums, low, locPivot); // 取到了locPivot
        quickSort(nums, locPivot + 1, high);
    }

    int partition(vector<int> & nums, int low, int high)
    {
        int p = rand() % (high - low + 1) + low;
        int pivot = nums[p];
        swap(nums[p], nums[low]);
        while(low < high)
        {
            while(low < high && nums[high] >= pivot) --high; //注意>=
            nums[low] = nums[high];
            while(low < high && nums[low] <= pivot) ++low; //注意<=
            nums[high] = nums[low];
        }
        nums[low] = pivot;
        return low;
    }
};

归并排序(MergeSort)

归并排序将数组分成两个子数组,分别对两个子数组排序,然后合并这两个有序数组。

归并排序有递归和迭代两种写法,下面分别给出这两种实现方式。归并排序最重要的合并两个有序数组的merge函数。

1. 递归(Recursive):自上而下
class Solution {
public:
    // 归并排序方法一:递归recursive,自上而下
    // 分三步:(1)终止条件;(2)把当前序列平分成两个子序列,分别进行递归排序;(3)用双指针对两个递增子序列的结果进行merge
    vector<int> sortArray(vector<int>& nums) {
        if(nums.empty()) return nums;
        vector<int> tmp(nums.size(), 0); //需要一个额外的辅助空间
        mergeSortRecursive(nums, 0, nums.size() - 1, tmp);
        return nums;
    }

    // 通用的merge函数:对nums[low,...,mid]和nums[mid+1,...,high]两个递增数组进行合并
    void merge(vector<int> & nums, int low, int mid, int high, vector<int> & tmp)
    {
        int i = low, j = mid + 1, k = low; // i是子序列1的起始位置,j是子序列2的起始位置,k是暂存数组tmp的索引
        while(i <= mid && j <= high)
        {
            if(nums[i] <= nums[j]) tmp[k++] = nums[i++];
            else tmp[k++] = nums[j++];
        }
        while(i <= mid) tmp[k++] = nums[i++];
        while(j <= high) tmp[k++] = nums[j++];
        for(int p = low; p <= high; ++p)
        {
            nums[p] = tmp[p];
        }
    }
    
    void mergeSortRecursive(vector<int>& nums, int low, int high, vector<int> & tmp)
    {
        if(low >= high) return ; //没有元素,或只有一个元素
        int mid = low + (high - low) / 2;
        mergeSortRecursive(nums, low, mid, tmp);
        mergeSortRecursive(nums, mid + 1, high, tmp);
        merge(nums, low, mid, high, tmp);
    }
};
2. 迭代(Iterative):自下而上
class Solution {
public:
    // 归并排序方法二:迭代iterative,自下而上
    // 分两步:(1)对子序列长度len从1开始进行2倍递增;(2)根据当前len计算得到相邻两个子序列的low、mid、high,进行merge;
    vector<int> sortArray(vector<int>& nums) {
        if(nums.empty()) return nums;
        vector<int> tmp(nums.size(), 0); //需要一个额外的辅助空间
        int n = nums.size();
        for(int len = 1; len < n; len *= 2) //len表示两两merge的子序列中,一个子序列的长度。从1,2,4,8这样两倍变化
        {
            for(int low = 0; low < n; low += 2*len) //low表示每两两merge的子序列中,第一个子序列的起始位置
            {
                int mid = min(low + len - 1, n - 1); // mid表示每两两merge的子序列中,第一个子序列的结束位置
                int high = min(low + 2 * len - 1, n - 1); //high表示每两两merge的子序列中,第二个子序列的结束位置
                merge(nums, low, mid, high, tmp);
            }
        }
        return nums;
    }

    // 通用的merge函数:对nums[low,...,mid]和nums[mid+1,...,high]两个递增数组进行合并
    void merge(vector<int> & nums, int low, int mid, int high, vector<int> & tmp)
    {
        int i = low, j = mid + 1, k = low; // i是子序列1的起始位置,j是子序列2的起始位置,k是暂存数组tmp的索引
        while(i <= mid && j <= high)
        {
            if(nums[i] <= nums[j]) tmp[k++] = nums[i++];
            else tmp[k++] = nums[j++];
        }
        while(i <= mid) tmp[k++] = nums[i++];
        while(j <= high) tmp[k++] = nums[j++];
        for(int p = low; p <= high; ++p)
        {
            nums[p] = tmp[p];
        }
    }
};

插入排序(InsertionSort)

插入排序时,i从左到右遍历整个数组,将nums[i]插入到[0, i-1]的已排序区间里该在的位置。插入方法是倒着交换过去。
在这里插入图片描述

void insertionSort(vector<int> & nums)
{
	for(int i = 0; i < nums.size(); ++i)
	{
		for(int j = i; j > 0; --j)
		{
			if(nums[j] < nums[j - 1]) swap(nums[j], nums[j - 1]);
		}
	}
}

冒泡排序(BubbleSort)

从左到右遍历数组,对相邻的两个元素进行比较,看是否左<=右,如果不满足就让它俩互换。一次冒泡会让最大的放到最后面,重复 n 次,就完成了 n 个数据的排序工作。当某次冒泡操作已经没有数据交换时,说明已经达到完全有序,不用再继续执行后续的冒泡操作。
一次冒泡:
在这里插入图片描述
n次冒泡:
在这里插入图片描述

void bubbleSort(vector<int> & nums)
{
	bool swapped;
	for(int i = 1; i < nums.size(); ++i)
	{
		swapped = false;
		for(int j = 1; j < nums.size() - i + 1; ++j)
		{
			if(nums[j] < nums[j - 1])
			{
				swap(nums[j], nums[j - 1]);
				swapped = true;
			}
		}
		if(!swapped)
		{
			break;
		}
	}
}

选择排序(SelectionSort)

选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

在这里插入图片描述

void selectionSort(vector<int> &nums)
{
	int mid;
	for(int i = 0; i < n - 1; ++i)
	{
		mid = i;
		for(int j = i + 1; j < n; ++j)
		{
			if (nums[j] < nums[mid])
			{
			mid = j;
			}
		}
		swap(nums[mid], nums[i]);
	}
}

K-th element相关题目

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

快速选择:快速选择是快速排序算法的一种变形应用,通常用来在未排序的数组中寻找第k小/第k大的元素。快速选择并不递归访问双边,而是只递归进入一边的元素中继续寻找,这降低了平均时间复杂度,从O(nlogn)至O(n),不过最坏情况仍然是O(n^2)。

class Solution {
public:
    int findKthLargest(vector<int>& nums, int k) {
        // 快速选择:二分查找 + 通过快排的partition函数来缩小查找区间
        int n = nums.size();
        int l = 0, r = n - 1, mid; 
        k = n - k; //第k大,是从右往左第k个,所以是从左往右第n-k个
        while(l <= r)
        {
            mid = partition(nums, l, r); //从nums的l到r里随机选一个数,将它排到自己应该在的位置mid上,[l, mid - 1]都是比它小的,[mid + 1, r]都是比它大的
            if(mid == k) return nums[k];
            else if(mid > k) r = mid - 1;
            else l = mid + 1;
        }
        return r;
    }

    int partition(vector<int>& nums, int l, int r)
    {
        int p = l + rand() % (r - l + 1); //随机选一个数作为pivot
        int pivot = nums[p];
        swap(nums[p], nums[r]); //把pivot放到末尾比较方便
        int i = l;
        for(int j = i; j < r; ++j)
        {
            if(nums[j] < pivot)
            {
                swap(nums[i], nums[j]);
                ++i;
            }
        }
        swap(nums[i], nums[r]);
        return i;
    }
};

leetcode 347. 前 K 个高频元素

桶排序:将n个元素通过hash分配到k个桶,使得每个桶内大约有5~15个元素,对各个桶内的元素通过插入排序或者递归桶排进行排序后,拼接合并在一起。平均时间复杂度O(n+k),适合分布均匀数组的排序。

class Solution {
public:
    vector<int> topKFrequent(vector<int>& nums, int k) {
        // 桶排序的桶是vector,因为各个桶是有序的。普通的桶可以是unordered_map,访问快。
        // 本题中,先用桶记录各数字的出现频率(这一步不是桶排序里的步骤);再通过桶排序对频率进行排序,具体做法是,按频率对数字进行分桶,因为题目要求频率k里的数字可以按任意顺序返回,所以桶内数字就不需要再进行插入排序了。Time O(n), Space O(n)
        unordered_map<int, int> count; // 数字:频率
        int maxCount  = 0; //记录下最大的频率,作为后续桶排序的分桶数
        for(int i = 0; i < nums.size(); ++i)
        {
            ++count[nums[i]];
            maxCount = max(maxCount, count[nums[i]]);
        }
        // 桶排序
        vector<vector<int>> bucket(maxCount + 1); // 频率:[数字1, 数字2]。对各数字按出现频率分桶,加1是为了使下标和频率对应起来,都是[1, maxCount]
        for(auto & ele : count)
        {
            bucket[ele.second].push_back(ele.first);
        }
        vector<int> res;
        for(int j = maxCount; j >= 1 && res.size() < k; --j)
        {
            for(auto &num : bucket[j]) res.push_back(num);
        }
        return res;
    }
};

本文的配图摘自极客时间APP的《数据结构与算法之美》课程插图。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值