目录
1.2快速排序拓展:基于快排的C++sort()函数(为什么你总是看不懂?)
一、快速排序
基础版:取最左元素为哨兵:
/* 元素交换 */
void swap(vector<int>& nums, int i, int j) {
int tmp = nums[i];
nums[i] = nums[j];
nums[j] = tmp;
}
/* 哨兵划分 */
int partition(vector<int>& nums, int left, int right) {
int pivot=nums[left];//nums[left]作为基准数
int i = left, j = right;
while (i < j) {
while (i < j && nums[j] >= pivot)
j--; // 从右向左找首个小于基准数的元素
while (i < j && nums[i] <= pivot)
i++; // 从左向右找首个大于基准数的元素
swap(nums, i, j); // 交换这两个元素
}
swap(nums, i, left); // 将基准数交换至两子数组的分界线
return i; // 返回基准数的索引
}
优化:随机选取一个哨兵:
//912. 排序数组-快速排序
class Solution {
public:
/*4、i指针指向<=哨兵的元素队列的最右边的元素的位置。*/
int partition(vector<int>& nums, int l, int r){
int pivot=nums[r]; //哨兵的值。
int i=l-1; //初始没排序,没有元素比哨兵小,i指向-1。
for(int j=l;j<=r-1;++j){ //j从l开始遍历,不是从0开始! j<=r-1因为nums[r]是哨兵,不用排它。
if(nums[j]<=pivot){ //如果遇到<=哨兵的元素。
i=i+1; //i+1,指向比第一个哨兵大的元素的位置,也是比哨兵小的最右边的元素的后一个位置。
swap(nums[i],nums[j]);//交换比哨兵大的元素和当前遍历的元素,当前遍历的元素就去到了哨兵小的元素的队列的最右边(比哨兵小的元素的队列长度加一了哟)。
}
}
swap(nums[i+1],nums[r]); //再把哨兵换回来。i+1,指向比第一个哨兵大的元素的位置。
return i+1; //返回哨兵的位置。
}
/*3、随机选一个哨兵,把哨兵与最后元素交换,返回排好后的哨兵的位置*/
int randomized_partion(vector<int>& nums, int l, int r){
int i=rand()%(r-l+1)+l; // 随机选一个作为我们的主元
swap(nums[i],nums[r]);
return partition(nums,l,r);
}
/*快排-1、将这一列排序,返回排序的哨兵的位置 2、并把这一列递归地【划分】为左右两列*/
void randomized_quicksort(vector<int>& nums, int l, int r){
if(l<r){
int pos=randomized_partion(nums,l,r);
randomized_quicksort(nums,l,pos-1);
randomized_quicksort(nums,pos+1,r);
}
}
/*主程序*/
vector<int> sortArray(vector<int>& nums) {
int n=nums.size();
randomized_quicksort(nums,0,n-1);
return nums;
}
};
1.2快速排序拓展:基于快排的C++sort()函数(为什么你总是看不懂?)
二、堆排序
堆排序部分引用/转载的是K大的《hello 算法》,原文地址:8.1. 堆 - Hello 算法
堆 (Heap)是一棵限定条件下的「完全二叉树」。根据成立条件,堆主要分为两种类型:
- 「大顶堆 Max Heap」,任意结点的值 ≥ 其子结点的值;
- 「小顶堆 Min Heap」,任意结点的值 ≤ 其子结点的值;
- 由于堆是完全二叉树,因此最底层结点靠左填充,其它层结点皆被填满。
- 二叉树中的根结点对应「堆顶」,底层最靠右结点对应「堆底」。
- 对于大顶堆 / 小顶堆,其堆顶元素(即根结点)的值最大 / 最小。
8.1.2. 堆常用操作¶
值得说明的是,多数编程语言提供的是「优先队列 Priority Queue」,其是一种抽象数据结构,定义为具有出队优先级的队列。
而恰好,堆的定义与优先队列的操作逻辑完全吻合,大顶堆就是一个元素从大到小出队的优先队列。从使用角度看,我们可以将「优先队列」和「堆」理解为等价的数据结构。因此,本文与代码对两者不做特别区分,统一使用「堆」来命名。
堆的常用操作见下表,方法名需根据编程语言确定。
方法名 | 描述 | 时间复杂度 |
---|---|---|
push() | 元素入堆 | �(log�) |
pop() | 堆顶元素出堆 | �(log�) |
peek() | 访问堆顶元素(大 / 小顶堆分别为最大 / 小值) | �(1) |
size() | 获取堆的元素数量 | �(1) |
isEmpty() | 判断堆是否为空 | �(1) |
8.1.3. 堆的实现¶
下文实现的是「大顶堆」,若想转换为「小顶堆」,将所有大小逻辑判断取逆(例如将 ≥ 替换为 ≤ )即可,有兴趣的同学可自行实现。
堆的存储与表示¶
在二叉树章节我们学过,「完全二叉树」非常适合使用「数组」来表示,而堆恰好是一棵完全二叉树,因而我们采用「数组」来存储「堆」。
二叉树指针。使用数组表示二叉树时,元素代表结点值,索引代表结点在二叉树中的位置,而结点指针通过索引映射公式来实现。
具体地,给定索引 i ,那么其左子结点索引为 2i+1 、右子结点索引为 2i+2 、父结点索引为 (i−1)/2 (向下整除)。当索引越界时,代表空结点或结点不存在。
元素入堆¶
给定元素 val
,我们先将其添加到堆底。添加后,由于 val
可能大于堆中其它元素,此时堆的成立条件可能已经被破坏,因此需要修复从插入结点到根结点这条路径上的各个结点,该操作被称为「堆化 Heapify」。
考虑从入堆结点开始,从底至顶执行堆化。具体地,比较插入结点与其父结点的值,若插入结点更大则将它们交换;并循环以上操作,从底至顶地修复堆中的各个结点;直至越过根结点时结束,或当遇到无需交换的结点时提前结束。
/* 元素入堆 */
void push(int val) {
// 添加结点
maxHeap.push_back(val);
// 从底至顶堆化
siftUp(size() - 1);
}
/* 从结点 i 开始,从底至顶堆化 */
void siftUp(int i) {
while (true) {
// 获取结点 i 的父结点
int p = parent(i);
// 当“越过根结点”或“结点无需修复”时,结束堆化
if (p < 0 || maxHeap[i] <= maxHeap[p])
break;
// 交换两结点
swap(maxHeap[i], maxHeap[p]);
// 循环向上堆化
i = p;
}
}
堆顶元素出堆¶
堆顶元素是二叉树根结点,即列表首元素,如果我们直接将首元素从列表中删除,则二叉树中所有结点都会随之发生移位(索引发生变化),这样后续使用堆化修复就很麻烦了。为了尽量减少元素索引变动,采取以下操作步骤:
- 交换堆顶元素与堆底元素(即交换根结点与最右叶结点);
- 交换完成后,将堆底从列表中删除(注意,因为已经交换,实际上删除的是原来的堆顶元素);
- 从根结点开始,从顶至底执行堆化;
顾名思义,从顶至底堆化的操作方向与从底至顶堆化相反,我们比较根结点的值与其两个子结点的值,将最大的子结点与根结点执行交换,并循环以上操作,直到越过叶结点时结束,或当遇到无需交换的结点时提前结束。
/* 元素出堆 */
void pop() {
// 判空处理
if (empty()) {
throw out_of_range("堆为空");
}
// 交换根结点与最右叶结点(即交换首元素与尾元素)
swap(maxHeap[0], maxHeap[size() - 1]);
// 删除结点
maxHeap.pop_back();
// 从顶至底堆化
siftDown(0);
}
/* 从结点 i 开始,从顶至底堆化 */
void siftDown(int i) {
while (true) {
// 判断结点 i, l, r 中值最大的结点,记为 ma
int l = left(i), r = right(i), ma = i;
// 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出
if (l < size() && maxHeap[l] > maxHeap[ma])
ma = l;
if (r < size() && maxHeap[r] > maxHeap[ma])
ma = r;
// 若结点 i 最大或索引 l, r 越界,则无需继续堆化,跳出
if (ma == i)
break;
swap(maxHeap[i], maxHeap[ma]);
// 循环向下堆化
i = ma;
}
}
8.1.4. 堆常见应用¶
- 优先队列。堆常作为实现优先队列的首选数据结构,入队和出队操作时间复杂度为 O(log n) ,建队操作为 O(n) ,皆非常高效。
- 堆排序。给定一组数据,我们使用其建堆,并依次全部弹出,则可以得到有序的序列。当然,堆排序一般无需弹出元素,仅需每轮将堆顶元素交换至数组尾部并减小堆的长度即可。
- 获取最大的 k 个元素。这既是一道经典算法题目,也是一种常见应用,例如选取热度前 10 的新闻作为微博热搜,选取前 10 销量的商品等。
三、归并排序
class Solution {
vector<int> tmp;
void mergeSort(vector<int>& nums, int l, int r) {
/*1.递归终止:左指针大于等于右指针*/
if (l >= r) return;
/*2.分组*/
int mid = (r-l)/2+l; //错误1:不是(l+r)/2-l....
mergeSort(nums, l, mid);
mergeSort(nums, mid + 1, r);
/*3.每组分组排序,用tmp临时储存,再一个个赋值给原数组nums*/
int i = l, j = mid + 1; //两组的指针
int cur = 0; //tmp指针,指出tmp数组的哪一位置来储存,储存两组中较小的元素
while (i <= mid && j <= r) { //当两组指针都没有到数组终点时
if (nums[i] <= nums[j]) {
tmp[cur] = nums[i];
cur++;i++;
}
else {
tmp[cur] = nums[j];
cur++;j++;
}
}
while (i <= mid) {
tmp[cur] = nums[i];
cur++;i++;
}
while (j <= r) {
tmp[cur] = nums[j];
cur++;j++;
}
/*再一个个赋值给原数组nums*/
for (int i = 0; i < r - l + 1; ++i) { //排序的元素个数:r-l+1
nums[l+i] = tmp[i]; //排序的位置从nums[l]到nums[r] //错误2:是temp[i]不是temp[cur]
}
}
public:
vector<int> sortArray(vector<int>& nums) {
tmp.resize((int)nums.size(), 0); //错误3:忘记写这个了 v.resize(amount,initial val)
mergeSort(nums, 0, (int)nums.size() - 1);
return nums;
}
};
归并排序的递归也可以写成不用return的形式:
void MergeSort(vector<int>& nums,vector<int>&temp,int l,int r){
if(l<r){
/*分组*/
int mid=(r-l)/2+l;
MergeSort(nums,temp,l,mid);
MergeSort(nums,temp,mid+1,r);
/*排序*/
mergesort(nums,temp,l,mid,mid+1,r);
}
}
归并排序的拓展(1):数组中的逆序对
剑指 Offer 51. 数组中的逆序对 - 力扣(Leetcode)
找出数组中的逆序对,可以用归并排序来遍历。其中最关键的一步是如何找跨组的逆序对。即逆序对一个数在[l,mid]中,一个数在[mid+1,r]中,并且保证统计是完全的、没有遗漏的。
方法:按照逆序对的要求,如果如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。i指向的组在j指向的组的前面,并且i的组和j的组已经各自按照升序排好序。也就是说,如果nums[i]>nums[j],nums[i]之后的数(直到nums[mid])又都是比nums[i]大的,说明nums[i]之后的数都大于nums[j],就找到了跨组的逆序对。并且这种找法是能够找全的。
举个例子:找出[7,5,6,4]的逆序对个数。
通过归并排序,第一轮排序:
在比较7和5时发现了一个逆序对[7,5],count++count=1,在比较[6,4]时发现了一个逆序对,count++,count=2。
第二轮排序:
在比较5和4时,5>4,即nums[i]>nums[j],并且[5,7]已经按升序排序,那么说明5后面到mid的数([7])都>4,这次比较找出的逆序对是[5,4]和[7,4],共有mid-i+1个,所以count+=mid-i+1个。所以这次count+=1-0+1=2,count=4,
在比较7和6时又出现nums[i]>nums[j],count+=1-1+1=1个,count=5
答案:5
统计逆序对个数的代码:
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) {
tmp[cnt++] = nums[i++];
}
else {
tmp[cnt++] = nums[j++];
count+=mid-i+1;//关键。见注释
}
其余的代码就是归并排序。
class Solution {
private:
public:
void mergeSort(vector<int>& nums, vector<int> &tmp,int &count,int l, int r) {
if (l >= r) return;
int mid = (r-l) /2+l;
mergeSort(nums,tmp,count, l, mid);
mergeSort(nums,tmp,count, mid + 1, r);
int i = l, j = mid + 1;
int cnt = 0;
while (i <= mid && j <= r) {
if (nums[i] <= nums[j]) {
tmp[cnt++] = nums[i++];
}
else {
//如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
tmp[cnt++] = nums[j++];
count+=mid-i+1;//关键。见注释
}
}
while (i <= mid) {
tmp[cnt++] = nums[i++];
}
while (j <= r) {
tmp[cnt++] = nums[j++];
}
for (int i = 0; i < r - l + 1; ++i) {
nums[i + l] = tmp[i];
}
}
int reversePairs(vector<int>& nums) {
vector<int> tmp;
int count=0;
tmp.resize((int)nums.size(), 0);
mergeSort(nums,tmp,count, 0, (int)nums.size() - 1);
return count;
}
};
归并排序的拓展(2):重要逆序对
描述
给定N个数的序列a1,a2,...aN,定义一个数对(ai, aj)为“重要逆序对”的充要条件为 i < j 且 ai > 2aj。求给定序列中“重要逆序对”的个数。
输入
本题有多个测试点,每个测试点分为两行:第一行为序列中数字的个数N(1 ≤ N ≤ 200000),第二行为序列a1, a2 ... aN(0 ≤a ≤ 10000000),由空格分开。N=0表示输入结束。
输出
每个测试点一行,输出一个整数,为给序列中“重要逆序对”的个数。
样例输入
10 0 9 8 7 6 5 4 3 2 1 0样例输出
16
注意不能把重要逆序对的判断写在判断nums[i]和nums[j]里面,会多一重循环,超时。
超时的答案:
#include <iostream>
using namespace std;
long long sum = 0;
void merge(int *s, int *temp, int startIndex, int endIndex, int mid)
{
int i = startIndex, j = mid + 1, k = startIndex;
int pointer = startIndex;
while(i <= mid && j <= endIndex)
{
if(s[i] > s[j])
{
temp[k] = s[j];
while(s[pointer] <= 2 * s[j] && pointer <= mid)
{
pointer ++;
}
if(pointer != mid + 1)
{
sum += mid - pointer + 1;
}
j ++;
}
else
{
temp[k] = s[i];
i ++;
}
k ++;
}
while(i <= mid)
{
temp[k ++] = s[i ++];
}
while(j <= endIndex)
{
temp[k ++] = s[j ++];
}
for(int i = startIndex; i <= endIndex; i ++)
{
s[i] = temp[i];
}
}
void mergeSort(int *s, int *temp, int startIndex, int endIndex)
{
int mid = (startIndex + endIndex) / 2;
if(startIndex < endIndex)
{
mergeSort(s, temp, startIndex, mid);
mergeSort(s, temp, mid + 1, endIndex);
merge(s, temp, startIndex, endIndex, mid);
}
}
int s[200005] = {};
int temp[400010] = {};
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i ++)
{
cin >> s[i];
}
mergeSort(s, temp, 0, n - 1);
cout << sum << endl;
return 0;
}
正确的写法是把重要逆序对的判断写在外面:
#include <iostream>
using namespace std;
int arr[200005];
int tmp[200005];
int N;
long long mergesort(int start,int end){
long long cnt=0, mid=(start+end)/2;
if(start>=end) return 0;
cnt+=mergesort(start,mid);
cnt+=mergesort(mid+1,end);
//find important reverse pair
int i=start,j=mid+1;
while(i<=mid && j<=end){
if(arr[i]>2*arr[j] && j<=end){
cnt+=(mid-i+1);
j++;
}else{
i++;
}
}
//merge sort
i=start,j=mid+1;
int idx=0;
while(i<=mid && j<=end){
if(arr[i]>arr[j]){
tmp[idx++]=arr[j++];
}else{
tmp[idx++]=arr[i++];
}
}
while(i<=mid) tmp[idx++]=arr[i++];
while(j<=end) tmp[idx++]=arr[j++];
for(int k=0;k<idx;k++){
arr[start+k]=tmp[k];
}
return cnt;
}
int main(){
while(cin>>N){
if(N==0) break;
for(int i=1;i<=N;i++){
cin>>arr[i];
}
cout<<mergesort(1,N)<<endl;
}
return 0;
}