算法基础2:递归排序算法和O(NlogN)复杂度

目录

一.递归行为

1.概念

2.注意事项

3.适用场景

4.master公式

二.归并排序

1.基本概念

2.(进阶)小和问题

3.逆序对问题

三.快速排序

1.荷兰国旗问题

2.快速排序


 

一.递归行为

1.概念

        一个函数直接或间接地调用自身的过程。递归可以用来解决可以分解为一系列相似子问题的问题,其中每个子问题都与原始问题具有相同的形式,但规模更小。

注意:递归过程中必须有一个或多个基本情况,这些情况不需要进一步递归就可以直接解决。基本情况防止无限递归,并为递归调用提供终止条件。

使用理由:

(1)简化问题解决过程:通过将问题分解成更小、更易于管理的子问题,递归可以简化复杂问题的解决方案。

(2)自然地模拟问题:某些问题(如树的遍历、图的搜索、分治算法)的结构天生适合于递归解决方案。

2.注意事项

终止条件:每个递归函数都必须有一个明确的终止条件,否则会导致无限递归,最终可能导致栈溢出错误。

性能考虑:虽然递归提供了一个优雅的解决方案,但在某些情况下,它可能不如迭代方法高效。递归可能会导致重复计算和较高的内存使用(由于维护递归调用的栈)。

尾递归优化:某些编译器可以优化尾递归调用,将其转换为迭代式来避免栈溢出问题。尾递归发生在函数返回时直接进行的递归调用。

 

3.适用场景

        递归特别适用于解决那些可以自然分解为相似子问题的问题,比如算法(如快速排序、归并排序)、数据结构(如二叉树的遍历、图的深度优先搜索),以及各种数学问题。通过理解递归的概念和正确应用,可以有效地解决许多编程和数学问题,但也需要注意其可能带来的性能和资源使用问题。

案例:过递归算法求数组的最大值

代码示例:

#include<iostream>
#include<string>
#include<math.h>
#include<vector>
#include<algorithm>
using namespace std;

int maxfind(const vector<int>& num, int left, int right);
int main() {
	vector<int> nums = { 1,3,5,7,9,2,4,6,8 };
	int len = nums.size();
	int value = maxfind(nums, 0, len - 1);
	cout << "数组最大值为:" << value << endl;
	system("pause");
	return 0;
}
int maxfind(const vector<int>& num, int left, int right) {
	/*int len = num.size();
	if (len == 0) return -1;
	if (len == 1) return num[0];*/
	if (left == right) return num[left];
	int mid = left + ((right - left) >> 1);//右移1位==除以2
	int leftmax = maxfind(num,left,mid);
	int rightmax = maxfind(num, mid + 1, right);
	return max(leftmax, rightmax);

}

4.master公式

        Master定理(Master Theorem)提供了一种快速求解递归关系式的方法,特别是那些出现在分治算法分析中的递归关系。这个定理可以直接给出许多分治递归算法的时间复杂度,无需通过复杂的递归树或者替换方法手动解算。

master公式:T(N) = a*T(N/b) + O(N^d)

例如:上面的递归数组求最大值代码:T(N) = 2 * T(N/2) + O(1)

T(N) :是问题规模为N的解的时间复杂度。

a:表示递归每一层的子问题数量。(a>=1)

N/b:表示子问题的规模,也就是问题大小缩小的比例。(b>1)

O(N^d):是在每一层递归上除了递归调用外其他操作的时间复杂度。

二.归并排序

1.基本概念

步骤:

1.递归分解:原始数组被递归地分解为两个子数组。具体来说,如果原始数组可以被视为 A[1...n],那么它被分解为 A[1...n/2]A[n/2+1...n]

2.基本情况处理:递归继续,直到达到基本情况,即子数组的大小为1或0。这样的子数组自然是有序的。

3.合并步骤:在每一层递归中,排序好的子数组会被合并成一个有序的数组。合并步骤是通过一个辅助函数完成的,该函数通过比较来自两个子数组的元素来构建一个有序数组。

归并过程:假设我们有两个已排序的子序列,需要将它们合并成一个有序序列。我们使用两个指针分别跟踪每个子序列的当前元素。在每一步中,我们比较两个指针所指向的元素,将较小的元素复制到一个辅助数组中,然后移动该元素所对应的指针。重复这一过程,直到所有元素都被复制到辅助数组中。最后,将辅助数组的内容复制回原数组,完成合并过程。

示例代码:由process和merge函数实现

#include<iostream>
#include<string>
#include<cmath> // 更正为 cmath
#include<vector>
#include<algorithm>
using namespace std;

// 函数原型声明
int maxfind(const vector<int>& num, int left, int right);
void process(vector<int>& num, int left, int right); // 引用传递
void merge(vector<int>& num, int left, int mid, int right); // 引用传递

int main() {
    vector<int> nums = { 1, 5, 3, 9, 7, 2, 6, 8, 4 };
    int len = nums.size();
    int value = maxfind(nums, 0, len - 1);
    process(nums, 0, len - 1);
    cout << "排序后的数组为:" << endl;
    for (int i = 0; i < len; i++) {
        cout << nums[i] << " "; // 修正输出格式,添加空格
    }
    cout << "\n数组最大值为:" << value << endl;
    system("pause");
    return 0;
}

int maxfind(const vector<int>& num, int left, int right) {
    if (left == right) return num[left];
    int mid = left + ((right - left) >> 1);
    int leftmax = maxfind(num, left, mid);
    int rightmax = maxfind(num, mid + 1, right);
    return max(leftmax, rightmax);
}

void process(vector<int>& num, int left, int right) {
    if (left >= right) return;
    int mid = left + ((right - left) >> 1);
    process(num, left, mid);
    process(num, mid + 1, right);
    merge(num, left, mid, right);
}

void merge(vector<int>& num, int left, int mid, int right) {
    vector<int> temp(right - left + 1);//*****
    int i = left, j = mid + 1, k = 0;

    while (i <= mid && j <= right) {
        if (num[i] <= num[j]) {
            temp[k++] = num[i++];
        }
        else {
            temp[k++] = num[j++];
        }
    }
    while (i <= mid) {
        temp[k++] = num[i++];
    }
    while (j <= right) {
        temp[k++] = num[j++];
    }
    for (i = left, k = 0; i <= right; i++, k++) {
        num[i] = temp[k];
    }
}

性能:归并排序的时间复杂度为O(nlogn),其中 n 是数组或列表的元素数量。这是因为每一层递归操作都需要遍历整个数组O(n)),而递归树的深度为 O(logn)。额外空间复杂度为O(n)。

master公式:T(n) = 2T(n/2)+O(n)

2.(进阶)小和问题

问题:"小和"指的是一个数组中,对于每个数左边比当前数小的数的总和。例如,在数组 [1, 3, 4, 2] 中,数字 3 的小和是 1,数字 4 的小和是 1 + 3 = 4,数字 2 的小和是 1,所以整个数组的小和是 1 + 4 + 1 = 6

解决思路:

        归并排序的分治策略可以被用来高效地计算整个数组的小和。在合并过程中,当我们比较两个子数组的元素以进行排序时,我们可以计算出小和:

假设我们正在合并两个已排序的子数组。每当我们从左侧子数组取出一个元素 x 并将其放入最终排序数组中时,我们可以知道右侧子数组中已经有多少个元素被复制过来了(假设是 n 个)。这意味着有 n 个元素比 x 大,因此 x 对总小和的贡献是 x * n

        通过在归并排序的合并步骤中累加这些贡献,我们可以在排序过程中同时解决小和问题,而不需要额外的复杂度,整体算法仍然保持在O(nlogn)。

示例代码:

#include <iostream>
#include <vector>
using namespace std;

int mergeAndCount(vector<int>& nums, int left, int mid, int right) {
    vector<int> temp(right - left + 1);
    int i = left, j = mid + 1, k = 0;
    int smallSum = 0;

    while (i <= mid && j <= right) {
        if (nums[i] < nums[j]) {
            // 计算小和
            smallSum += (right - j + 1) * nums[i];
            temp[k++] = nums[i++];
        } else {
            temp[k++] = nums[j++];
        }
    }

    // 复制剩余的元素
    while (i <= mid) {
        temp[k++] = nums[i++];
    }
    while (j <= right) {
        temp[k++] = nums[j++];
    }

    // 将排序好的临时数组复制回原数组
    for (i = left, k = 0; i <= right; ++i, ++k) {
        nums[i] = temp[k];
    }

    return smallSum;
}

void process(vector<int>& nums, int left, int right, int& totalSmallSum) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        process(nums, left, mid, totalSmallSum);
        process(nums, mid + 1, right, totalSmallSum);
        totalSmallSum += mergeAndCount(nums, left, mid, right);
    }
}

int smallSum(vector<int>& nums) {
    int totalSmallSum = 0;
    process(nums, 0, nums.size() - 1, totalSmallSum);
    return totalSmallSum;
}

int main() {
    vector<int> nums = {1, 3, 4, 2};
    cout << "The small sum is: " << smallSum(nums) << endl;
    return 0;
}

3.逆序对问题

问题:一个数组中,左边的数如果比右边的数大,则折两个数构成一个逆序对,请打印所有的逆序对。

解决思路:与小和问题类似,解决逆序对问题的关键在于归并排序的合并步骤。当合并两个已排序的子数组时,如果从右侧子数组中取出一个元素并将其放入合并结果之前,那么左侧子数组中所有剩余的元素都将与此元素形成逆序对,因为它们都大于右侧的这个元素且在原始数组中的位置都在它之前。

#include<iostream>
#include<vector>
using namespace std;

int merge(vector<int>& nums, int left, int mid, int right) {
    vector<int> temp(right - left + 1);
    int i = left, j = mid + 1, k = 0;
    int inversions = 0;

    while (i <= mid && j <= right) {
        if (nums[i] <= nums[j]) {
            temp[k++] = nums[i++];
        } else {
            // 当前左侧元素大于右侧元素,计算逆序对
            temp[k++] = nums[j++];
            inversions += (mid - i + 1); // 关键步骤
        }
    }

    while (i <= mid) temp[k++] = nums[i++];
    while (j <= right) temp[k++] = nums[j++];

    for (i = left, k = 0; i <= right; ++i, ++k) {
        nums[i] = temp[k];
    }

    return inversions;
}

int mergeSortAndCount(vector<int>& nums, int left, int right) {
    if (left >= right) return 0;
    int mid = (left + right) / 2;
    int inversions = mergeSortAndCount(nums, left, mid);
    inversions += mergeSortAndCount(nums, mid + 1, right);
    inversions += merge(nums, left, mid, right);
    return inversions;
}

int countInversions(vector<int>& nums) {
    return mergeSortAndCount(nums, 0, nums.size() - 1);
}

int main() {
    vector<int> nums = {1, 3, 2, 3, 1};
    cout << "Number of inversions: " << countInversions(nums) << endl;
    return 0;
}

三.快速排序

1.荷兰国旗问题

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

解决思路:

初始化两个指针,j指向数组的开始位置,用于记录小于等于num的区域边界;i用于遍历数组。遍历数组,i从头到尾移动。此时只有两种情况:

1)如果nums[i]小于等于num,则将nums[i]与nums[j]交换,i和j都向右移动一位。

2)如果nums[i]大于num,i向右移动一位,j不动。

遍历完成后,数组就被分为小于等于num的部分和大于num的部分。

注意:该方法不会对数组进行排序。

代码示例:

#include<iostream>
#include<string>
#include<vector>
using namespace std;

void helan(vector<int>& nums,int len,int num) {
	//vector<int>left = {};
	int temp;
	int j = 0;
	for (int i = 0; i < len; i++) {
		if (nums[i] <= num) {
			temp = nums[j];
			nums[j] = nums[i];
			nums[i] = temp;
			j++;
        else{
            continue;}
		}
	}
}

int main() {
	vector<int> nums = { 5, 3, 2, 6, 4, 1, 3, 7 };
	int len = nums.size();
	int num = 6;
	//调用函数
	helan(nums, len, num);
	cout << "排序后的数组是:" << endl;
	for (int i = 0; i < len; i++) {
		cout << nums[i] << " ";
	}
	system("pause");
	return 0;
}

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

解决思路:

初始化3个指针,i用于遍历数组,j用于记录小于num的区域边界,k用于记录大于num的区域边界。此时有3种情况:

1)如果nums[i]小于num,则将nums[i]与nums[j]交换,i和j都向右移动一位。

2)如果nums[i]等于num,则不作操作,i++。

3)如果nums[i]大于num,则将nums[k]与nums[i]交换,i不动(因为交换过来的是一个新的数),k--。

由于操作了右侧的数组,所以i<=k时就已经完成遍历。

注意:该方法不会对数组进行排序。

#include<iostream>
#include<string>
#include<vector>
using namespace std;

void helan(vector<int>& nums,int len,int num) {
	//vector<int>left = {};
	int temp;
	int i = 0;
	int k = len - 1;
	int j = 0;
	for (int i = 0; i <= k; i++) {
		
		if (nums[i] < num) {
			temp = nums[j];
			nums[j] = nums[i];
			nums[i] = temp;
			j++;
		}
		
		else if(nums[i] > num)
		{
			temp = nums[k];
			nums[k] = nums[i];
			nums[i] = temp;
			k--;
			i--;
		}
		else {
			continue;
		}
			
		
	}
	/*while (i <= k) {
		if (nums[i] < num) {
			swap(nums[i], nums[j]);
			j++;
			i++;
		}
		else if (nums[i] > num) {
			swap(nums[i], nums[k]);
			k--;
		}
		else {
			i++;
		}
	}*/
}

int main() {
	vector<int> nums = { 3,5,6,3,4,5,2,6,9,0 };
	int len = nums.size();
	int num = 5;
	//调用函数
	helan(nums, len, num);
	cout << "排序后的数组是:" << endl;
	for (int i = 0; i < len; i++) {
		cout << nums[i] << " ";
	}
	system("pause");
	return 0;
}

2.快速排序

基本思路

与荷兰国旗问题类似

1.选择一个元素作为"pivot"(基准),通常选择最右边的元素。

2.重新排列数组,所有比pivot小的元素都移动到pivot的左边,所有比pivot大的元素都移动到pivot的右边。这一步完成后,pivot元素将出现在其最终位置上。

3.对pivot左右两边的子数组递归执行以上操作。

#include<iostream>
#include<string>
#include<vector>
#include<random>
using namespace std;


void partation(vector<int>& nums, int left, int right) {
	if (left >= right) return;
	int j = left-1;
	int pivot = nums[right];
	for (int i = left; i <right; i++) {
		if (nums[i] < pivot) {
			j++;
			swap(nums[i], nums[j]);
			
		}
		
	}
	swap(nums[j + 1], nums[right]);
	partation(nums, left, j);
	partation(nums, j + 2, right);//pivot在j+1位置
}

void quicksort(vector<int>& nums, int left, int right) {
	if (left < right) {
		partation(nums, left, right);
	}
}
int main() {
	vector<int> nums = { 3,5,6,3,4,5,2,6,9,0 };
	int len = nums.size();
	quicksort(nums, 0, len - 1);
	//quicksort(nums, 0, len - 1);
	cout << "quick sort result" << endl;
	for (int i = 0; i < len; i++) {
		cout << nums[i] << " ";
	}
	system("pause");
	return 0;
}

注意:代码的核心在于partation函数,它通过交换元素,将数组分成两部分,并确保基准元素(pivot)位于其最终位置。递归的快速排序保证了数组的每一部分都被排序。这种方法的平均时间复杂度是O(nlog n)但最坏情况下时间复杂度可以退化到O(n^2)。选择不同的基准元素(如使用随机化选择)可以帮助避免最坏情况的发生。

快速排序2:

基本方法类似,采用两个指针从左右两侧开始往中间遍历,满足左<pivot且右>pivot,则交换两数的方法。

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

void quicksort(vector<int>& nums, int left, int right) {
	if (left >= right) return;
	int i = left;
	int j = right - 1;
	int pivot = nums[right];
	while (i <= j) {
		while (i <= j && nums[i] < pivot) i++;
		while (i <= j && nums[j] > pivot) j--;
		if (i <= j) {
			swap(nums[i],nums[j]);
			i++;
			j--;

		}		
	}
	swap(nums[i], nums[right]);
	quicksort(nums,left, i - 1);
	quicksort(nums, i + 1, right);
}


int main() {
	vector<int> nums = { 5,8,9,3,1,2,7,6,4 };
	int len = nums.size()-1;
	quicksort(nums, 0, len);
	cout << "sorted array:" << endl;
	for (int i = 0; i < len; i++) {
		cout << nums[i] << " ";
	}

	system("pause");
	return 0;
}

这个快速排序算法的平均时间复杂度也是O(nlogn),n其中是数组的长度。这是因为快速排序平均情况下将数组分为两个大致相等的部分,并对这两部分分别进行排序。因此,它在每一层递归中处理的元素数量大约减半,导致递归的深度大约是logn。在每一层递归中,算法都会遍历当前段的所有元素来进行分区,这需要O(n)时间。因此,总的平均时间复杂度是O(nlogn)。

最坏情况下的时间复杂度是O(n2)。这种情况发生在每次选择的枢轴都是当前数组中的最小或最大元素时,导致分区非常不平衡。在这种情况下,快速排序会退化成一个每次只减少一个元素的过程,因此需要进行n层递归,每层递归仍然需要遍历当前的所有元素进行分区,导致总的时间复杂度增加到O(n2)

 

 

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值