左神算法-基础班-class 2

本文介绍了快速排序的荷兰国旗问题优化版,包括经典快排、改进版(避免重复划分)和随机快排,以及堆排序的基本原理、堆插入与堆ify操作,以及堆排序完整流程。通过实例演示了如何实现大根堆和小根堆,以及如何结合这两种算法进行高效排序。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Reference

一、快排

1.1 荷兰国旗问题引入

分析

1.把数组划分为小于等于num的区域和大于num的区域。用变量x代表小于区域的位置。刚开始由于不存在小于等于的区域,则x = -1,表示指向数组-1(不存在)。再设置变量cur作为遍历的位置,刚开始cur = 0指向数组第一个数。

2.开始遍历
(1)如果cur<=num ,那么把cur当前的元素和小于等于区域的下一个数交换,即++x的位置和cur位置的数交换,cur,x后移;
(2)如果cur>num,cur后移;
(3)如果cur == arr_length结束遍历。

eg [4,6,7,3],num = 5
1.cur 指向4,4<5,把4和x的下一位即0号位置4(自己和自己)交换。之后x指向4,cur++,cur指向6;
2.cur指向6,5<6,x不变,cur++;
3.cur指向7,5<7,x不变,cur++;
4.cur指向3,3<5,交换,x的下一位即6和3交换。之后x指向3,cur++,退出循环。
此时,x指向的3以及之前的4为小于等于区域,而x之后的7,6为大于区域。

/******************************************************************************
 * 问题一:荷兰国旗问题引入
 * 给定一个数组arr,和一个数num,请把小于等于num的数放在数组的左边,大于num的数放在数组的右边。
 * 要求额外空间复杂度O(1),时间复杂度O(N)
 ******************************************************************************/
/*
  * @brief partition函数和快排中的partion一样
  * @param[in] arr 输入数组
  * @param[in] num 需要比较的值
  * @return void
  */
void partition(vector<int> &arr, int num)
{
    int x = -1; //代表小于区域的位置
    int cur = 0; //变量cur作为遍历的位置
    while (cur < arr.size()) {
        if (num >= arr[cur]) {
            swap(arr[cur++], arr[++x]);
        } else { //num < arr[cur]
            cur++;
        }
    }
}

1.2 荷兰国旗问题及复杂度分析

给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,等于num的数放在数组的中间,大于num的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)

分析

和预备问题相似
1.把数组划分为小于num的区域、等于num的区域和大于num的区域。用变量less代表小于区域的位置、more代表大于num的区域。刚开始由于不存在小于和大于的区域,则less = l-1,more = r + 1,表示指向数组l-1、r + 1(不存在)。再设置变量cur作为遍历的位置,刚开始cur = l指向数组第一个数。
2.开始遍历
(1)如果cur<num ,那么把cur当前的元素和小于区域的下一个数交换,即++x的位置和cur位置的数交换,cur,x后移;
(2)如果cur>num,那么把cur当前的元素和大于区域的前一个数交换,即–y的位置和cur位置的数交换,y前移,cur不变(因为交换过来的数不确定)
(3)如果cur == num,cur++。
(4)如果cur == more结束。

实现


/******************************************************************************
 * 问题二(荷兰国旗问题)
 * 给定一个数组arr,和一个数num,请把小于num的数放在数组的左边,
 * 等于num的数放在数组的中间,大于num的数放在数组的右边。
 * 要求额外空间复杂度O(1),时间复杂度O(N)
 ******************************************************************************/
 /*
  * @brief partition函数和快排中的partion一样
  * @param[in] arr 输入数组
  * @param[in] l  需要处理区间的最小值
  * @param[in] r 需要处理区间的最大值
  * @param[in] num 需要比较的值
  * @return
  */
 vector<int> partition(vector<int> &arr, int l, int r, int num)
 {
    int less = l - 1; //变量less代表小于区域的位置
    int more = r + 1; //变量more代表大于num的区域
    int cur = l; //变量cur作为遍历的位置
    while (cur < more) {
        if (num > arr[cur]) {
            swap(arr[++less], arr[cur++]);
        } else if (num < arr[cur]) {
            swap(arr[cur], arr[--more]);
        } else { // == num
            cur++;
        }
    }
    return vector<int> {less + 1, more - 1};
 }

 // for test
 int main ()
 {
     vector<int> input{2,5,9,6,2,3,4,2,7,1,3,0,0};
     vector<int> res;
     res = partition(input,0,12,3);
     for (int i = 0; i < input.size(); ++i) {
         cout << input[i] << " ";
     }
     cout<< endl;
     cout << res[0] << endl << res[1] <<endl;
 }

1.3 排序算法–快排(经典快排,改进快排,随机快排)

在这里插入图片描述

  • 首先在数组[l,r]中把最后一个元素作为num进行比较,得到两段区域,分别为小于等于最后一个数的区域和大于最后一个数的区域。
  • 在上述第一段区域内,最后一个数的值为num保留不动,倒数第二个数重新命名为num,接着继续比较,不断迭代。
  • 在上述1的第二段区域内,把最后一个数命名为num,接着继续比较,不断迭代。
  • 迭代继续的条件是if(l<r)

在这里插入图片描述

/**
 * brief  经典快速排序,思路和荷兰国旗问题中的预备题目一样:
 *           1.首先在数组[l,r]中把最后一个元素作为num进行比较,得到两段区域,分别为小于等于最后一个数的区域和大于最后一个数的区域。
             2.在上述第一段区域内,最后一个数的值为num保留不动,倒数第二个数重新命名为num,接着继续比较,不断迭代。
             3.在上述1的第二段区域内,把最后一个数命名为num,接着继续比较,不断迭代。
             4.迭代继续的条件是if(l<r)
 * @param[in] arr 输入数组
 * @param[in] l  需要处理区间的最小值
 * @param[in] r 需要处理区间的最大值
 * return 返回值为less:返回小于等于num的范围内最后一个元素的位置(最后一个位置就是原来的num),
 *       下次直接用倒数第二个位置元素(小于等于num区间的倒数第二个元素)作为下一次的num。
 */
int partition(vector<int> arr, int l, int r )
{
    int num = arr[r]; // 把最后一个数设为num进行比较
    int less = l - 1; // less为小于等于的区域,设为区域之前一个位置
    int cur = l; // cur是当前遍历的指针
    while (cur < r  + 1) { // 遍历所有位置
        if (arr[cur] <= num) {
            swap(arr[cur++], arr[++less]);
        } else {
            cur++;
        }
        return less;
    }
}
// 迭代部分
void QuickSort(vector<int> arr, int l, int r)
{
    if (l < r) { // 迭代的条件是在[l,r]范围内,如果越界表示不可继续分
        int pivot = partition(arr, l, r);
        QuickSort(arr, l, pivot - 1);
        QuickSort(arr, pivot + 1, r);
    }
}

/**
 * brief: 荷兰国旗改进的经典快排
 *       经典快排的缺点:每次只找出一个num进行排序,如果存在多个相同值的num还需要继续划分,多做了无用功。
 *                   考虑如果使用荷兰国旗的排序方法,将相同的num一次找出来,那么时间复杂度的常数时间就可以缩短。
 *                   荷兰国旗问题需要有两个指针记录小于num的范围和大于num的范围,考虑定义一个长度为2的数组记录下来,返回数组的指针用作下一步。
 * @param[in] arr 输入数组
 * @param[in] l  需要处理区间的最小值
 * @param[in] r 需要处理区间的最大值
 * @return 存放等于num值的索引最小值最大值的数组
 */

// 返回arr[r]在排序后的索引区间值
vector<int> partition(vector<int> arr, int l, int r)
{
    int num = arr[r]; // 首先在数组[l,r]中把最后一个元素作为num进行比较
    int less = l - 1; // less为小于等于num区间的之前的一个位置
    int more = r + 1;
    int cur = l;
    while (cur < more) {
        if (arr[cur] < num) {
            swap(arr[++less], arr[cur++]);
        } else if (arr[cur] > num) {
            swap(arr[cur], arr[--more]); // 此时cur并没有+1,因为交换后arr[cur]与num的大小关系未知
        } else {
            cur++;
        }
        return vector<int> {less + 1, more - 1};
    }
}

// 分治法迭代
void QuickSort(vector<int> arr, int l, int r)
{
    if (l < r) {
        vector<int> index = partition(arr, l, r); // index存储划分点
        QuickSort(arr, l, index[0] - 1); // [l, res[0] - 1]左子区间
        QuickSort(arr, index[1] + 1, r); // [res[1] + 1, r]右子区间
    }
}


/**
 * brief: 随机快排。
 *        荷兰国旗改进的经典快排缺点在于使用最后一个数作为num,
 *        例如[1,2,3,4,5,6]中复杂度就很高,因为此时复杂度就与数据状况有关了。
 *        考虑:在数组中随机选一个数作为num,这样就可以绕开原始数据状况,复杂度变成长期的期望值。
 */
// 分治法迭代
void QuickSort(vector<int> arr, int l, int r)
{
    if (l < r) {
        swap(arr[l+rand()%(r-l+1)], arr[r]); // 随机快排
        vector<int> index = partition(arr, l, r); // index存储划分点
        QuickSort(arr, l, index[0] - 1); // [l, res[0] - 1]左子区间
        QuickSort(arr, index[1] + 1, r); // [res[1] + 1, r]右子区间
    }
}


/**
    复杂度分析:
        时间复杂度为度O(NlogN),额外空间复杂度O(logN)
        1.对于长期选择来说,随机成中间的值可能性最大,分治思想T(n) = 2T(N/2) + O(n),时间复杂度为度O(NlogN)。
        2.随机查找时,二分多少次多少个断点,最好情况O(logN),最差为O(N),概率上长期为O(logN)。
 */


/** 
 * brief 经典快速排序(二),基准数据找其正确索引位置的过程
 *       partition 函数找到分区后的第一个元素正确索引的位置
 * @param[in] arr 输入数组
 * @param[in] low 需要排序区间的下限值
 * @param[in] high 需要排序区间的上限值
 */

// 找到arr[low]的正确索引
int partition2(vector<int> arr, int low, int high)
{
    int pivot = arr[low]; // 设最开始的基准数据为数组第一个元素arr[low],则首先用一个临时变量去存储基准数据
    while (low < high) {
        while (low < high & arr[high] > pivot) { // 当队尾的元素大于等于基准数据时,向前挪动high指针
            high--;
        }
        arr[low] = arr[high]; // 如果队尾元素小于基准数据了,需要将其赋值给low
        while (low < high & arr[low] <= pivot) { // 当队首元素小于等于pivot时,向前挪动low指针
            low++;
        }
        arr[high] = arr[low]; // 当队首元素大于pivot时,需要将其赋值给high
    }
    arr[low] = pivot; // 跳出循环时low和high相等,此时的low或high就是pivot的正确索引位置
    return low; // 返回pivot的正确位置
}

void QuikSort(vector<int> arr, int low, int high)
{
    if (low < high) {
        int pivot = partition2(arr, low, high); // 找arr[low]正确索引的位置
        QuikSort(arr, low, pivot); // 分治法递归左半部分
        QuikSort(arr, pivot + 1, high); // 分治法递归右半部分
    }
}

二、堆 & 堆排序

2.1 基础知识

1.完全二叉树有两种:

5
/
1 2
/ \ /
3 4 2 6
满二叉树

5
/
1 2
/
1
从左到右排序的二叉树

2.完全二叉树可等价为堆,堆可用数组实现
其中第i结点的左孩子:2i+1,第i结点的右孩子:2i+2,第i结点的父节点:(i-1)/2

3.大根堆:指任意子树的最大值是其头部的结点
小根堆:指任意子树的最小值是其头部的结点

2.2 heapInsert:新结点加入进来并向上调整为大根堆的过程

把数组变为大根堆,建立堆结构

分析

如果当前插入的元素大于其父节点的元素,那么交换父节点与当前节点的位置。接着考察更换结点后的插入元素与新位置的父节点大小,如果还是大于还要继续交换。
eg。例[2,1,3,6]变换为大根堆
位置0,1,2,3

  • 2插入,父节点(0-1)/2 = 0位置,自己和自己不动;
  • 1插入,父节点(1-1)/2 = 0位置,1<2,不动;
  • 3插入,父节点(2-1)/2 = 0位置,3>2,交换位置;此时3的位置变为0,父节点(0-1)/2 = 0位置,自己和自己不动;
    此时:

3
/
1 2

  • 6插入,父节点(3-1)/2 = 1位置,6>1,交换位置;此时6的位置变为1,父节点(1-1)/2 = 0位置;6>3,继续交换;

6
/
3 2
/
1

核心代码

(index-1)/2index父节点的位置,index是新插入节点在数组中的下标

void heapinsert(int arr[],int index)
{
	while(arr[index] > arr[(index-1)/2])
	{
		swap(arr[index],arr[(index-1)/2]);
		index = (index-1)/2;
	}
}

使用for循环遍历数组,调用heapinsert

for(int i = 0;i < length;i++ )
	{
		heapinsert(arr,i); // 用for循环传入要处理的index
	} 


#include<iostream>

#define length 5
using namespace std;

void swap(int &a,int &b)
{
    int temp = a;
    a = b;
    b = temp;

}
void heapinsert(int arr[],int index)
{
    while(arr[index] > arr[(index-1)/2])
    {
        swap(arr[index],arr[(index-1)/2]);
        index = (index-1)/2;
    }
}

int main()
{
    //int arr[length] = {2,1,3,6,0,4};
    int arr[length] = {3,4,5,1,2};
    int heapsize = length;
    for(int i = 0;i < heapsize;i++ )
    {
        heapinsert(arr,i);//用for循环传入要处理的index
    }

    system("pause");
    return 0;
}

2.3 heapify:假设数组中一个值变小了,重新调整为大根堆的过程

分析

  • 找出这个数的左右孩子中的最大值,将这个最大值与改变的值进行比较,如果改变的值大于左右孩子的最大值,则交换两个数,接着找到交换后的左右孩子,继续比较。
  • 继续比较的条件:左孩子未越界。

核心代码

比较左右孩子和当前的数,找出最大值赋给largest;如果当前数是largest则跳出,如果不是,则交换两个数,更新index,和左孩子位置,继续比较。

/**
 * brief  假设数组中一个值变小了,重新调整为大根堆的过程:
 *        比较左右孩子和当前的数,找出最大值赋给largest;如果当前数是largest则跳出,
 *        如果不是,则交换两个数,更新index,和左孩子位置,继续比较。
 * @param[in] index 需要调整的元素的下标
 * @param[in] heapsize [0, heapsize-1]区间为堆的区间,堆的长度一定小于等于arr.size()
 * @return void
 */
void heapify(int arr[],int index,int heapsize)
{
    int left = index * 2 + 1;
    while (left < heapsize) { // 左孩子没有越界
        int largest = left + 1 < heapsize && arr[left]<arr[left + 1] ? // 有右孩子(右孩子没有越界)&&
                      left + 1:left;
        largest = arr[largest] > arr[index] ? largest : index;
        if (largest == index) { // 虽然变了,但依然满足最大堆,直接退出
            break;
        }
        swap(arr[largest],arr[index]);
        index = largest;
        left =  index * 2 + 1;
    }
}

2.4 堆排序

HeapInsert结合Heapify。

(1)让数组变为大根堆(heapinsert);
(2)让最后一位和堆顶交换,堆大小减一(保存最大值)heapsize–;
(3)再重新变为大根堆,相当于把堆顶元素变小再重排(heapify)
(4)直到堆大小为1停止。

/************************************************************************************
 * 堆排序
 ************************************************************************************/
/**
 * brief  假设数组中一个值变小了,重新调整为大根堆的过程:
 *        比较左右孩子和当前的数,找出最大值赋给largest;如果当前数是largest则跳出,
 *        如果不是,则交换两个数,更新index,和左孩子位置,继续比较。
 * @param[in] index 需要调整的元素的下标
 * @param[in] heapsize [0, heapsize-1]区间为堆的区间,堆的长度一定小于等于arr.size()
 * @return void
 */
void Heapify(vector<int> arr,int index,int heapsize)
{
    int left = index * 2 + 1;
    while (left < heapsize) { // 左孩子没有越界
        int largest = left + 1 < heapsize && arr[left]<arr[left + 1] ? // 有右孩子(右孩子没有越界)&&
                      left + 1:left;
        largest = arr[largest] > arr[index] ? largest : index;
        if (largest == index) { // 虽然变了,但依然满足最大堆,直接退出
            break;
        }
        swap(arr[largest],arr[index]);
        index = largest;
        left =  index * 2 + 1;
    }
}

// 建初始堆:新结点加入进来并向上调整为大根堆的过程
void HeapInsert(vector<int> arr,int index)
{
    while(arr[index] > arr[(index-1)/2]) {
        swap(arr[index],arr[(index-1)/2]);
        index = (index-1)/2;
    }
}

/*
 * 堆排序流程:
 *   (1)建立初始堆;
 *   (2)将堆顶元素与堆中最后一个元素交换,剔除最后一个元素(HeapSize减少1),调整新堆
 */
void HeapSort(vector<int> arr)
{
    if (arr.empty() || arr.size() < 2) {
        return;
    }
    for (int i = 0; i < arr.size(); ++i) {
        HeapInsert(arr, i);
    }
    int len = arr.size();
    swap(arr[0], arr[--len]);
    while (len > 0) {
        Heapify(arr, 0, len);
        swap(arr[0], arr[--len]);
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值