算法|排序算法|选择|冒泡|插入|归并|快排|堆排|计数|C++编写|小和问题|逆序对问题|荷兰国旗问题|几乎有序的数组

目录

一、时间复杂度

1.1 常数时间的操作

一个操作与样本数据量没有关系,每次都是在固定时间内完成的操作,叫做常数操作

1.2 时间复杂度

  • 一个算法流程中,常数操作数量的指标,常用O 来表示,读作big O

  • 常数操作数量可进一步用表达式来表示。

  • 表达式只要高阶项,不要低阶项,也不要高阶项的系数。

二、排序算法及其复杂度

2.1 选择排序及其复杂度●●

2.1.1算法步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

2.1.2 代码

#include <iostream>

using namespace std;

void selectSort(int* arr, int L)
{
    if (arr == NULL || L < 2)
    {
        return;
    }
    for (int i = 0; i < L - 1; i++)
    {
        int minIndex = i;
        for (int j = i + 1; j < L; j++)
        {
            if (arr[j] < arr[minIndex])
            {
                minIndex = j;
            }
        }
        int temp = arr[minIndex];
        arr[minIndex] = arr[i];
        arr[i] = temp;
    }
}

int main()
{
    int arr[5] = {5,4,3,2,1};
    for (int i = 0; i < 5; i++)
    {
        cout << arr[i] <<" ";
    }
    selectSort(arr, 5);

    cout << endl;
    for (int i = 0; i < 5; i++)
    {
        cout << arr[i] << " ";
    }
}

2.1.3 复杂度

f ( n ) = ( n − 1 ) + ( n − 2 ) + . . . + 2 + 1 + 0 = ( 0 + ( n − 1 ) ) n 2 = n 2 − n 2 = n 2 2 − n 2 f(n)=(n-1) + (n-2) + ...+ 2 + 1 + 0 = \frac{(0+(n-1))n}{2}=\frac{n^2-n}{2}=\frac{n^2}{2} - \frac{n}{2} f(n)=(n1)+(n2)+...+2+1+0=2(0+(n1))n=2n2n=2n22n

所以时间复杂度为: O ( n 2 ) O(n^2) O(n2)

空间复杂度为: O ( 1 ) O(1) O(1)

2.2 冒泡排序及其复杂度●●

2.2.1 算法步骤

  1. 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。
  4. 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

2.2.2 代码

#include <iostream>

using namespace std;

void bubbleSort(int *arr, int L)
{
	if (arr == NULL || L < 2)
		return;
	for (int e = L - 1; e > 0; e--)
	{
		for (int i = 0; i < e; i++)
		{
			if (arr[i] > arr[i + 1])
			{
				int temp = arr[i];
				arr[i] = arr[i + 1];
				arr[i + 1] = temp;
			}
		}
	}
}

int main()
{
	int arr[5] = {5,4,3,2,1};
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	cout << endl;
	bubbleSort(arr, 5);
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
}

2.2.3 复杂度

f ( n ) = n + ( n − 1 ) + ( n − 2 ) + . . . + 2 + 1 = ( 1 + n ) n 2 = n 2 2 + n 2 f(n) = n + (n-1) + (n-2) + ... + 2 + 1 = \frac{(1+n)n}{2} = \frac{n^2}{2} + \frac{n}{2} f(n)=n+(n1)+(n2)+...+2+1=2(1+n)n=2n2+2n

所以时间复杂度为: O ( n 2 ) O(n^2) O(n2)

空间复杂度为: O ( 1 ) O(1) O(1)

2.2.4 复习次数

2次

2.3插入排序及其复杂度●●

2.3.1 算法步骤

  1. 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
  2. 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

2.3.2 代码

#include <iostream>

using namespace std;

void insertSort(int* arr, int L)
{
	if (arr == NULL || L < 2)
		return;
	for (int i = 1; i < L; i++)
	{
		for (int j = i - 1; j >= 0; j--)
		{
			if (arr[j+1] < arr[j])
			{
				int temp = arr[j + 1];
				arr[j+1] = arr[j];
				arr[j] = temp;
			}
		}
	}
}

int main()
{
	int arr[5] = { 5,4,3,2,1 };	//45321 43521
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	cout << endl;
	insertSort(arr, 5);
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
}

2.3.3 时间复杂度

f ( n ) = 1 + 2 + 3 + . . . + ( n − 2 ) + ( n − 1 ) + n = n 2 2 + n 2 f(n)=1+2+3+...+(n-2)+(n-1)+n = \frac{n^2}{2} + \frac{n}{2} f(n)=1+2+3+...+(n2)+(n1)+n=2n2+2n

所以时间复杂度为: O ( n 2 ) O(n^2) O(n2)

空间复杂度为: O ( 1 ) O(1) O(1)

2.4 归并排序●●

2.4.1 算法步骤

  1. 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
  2. 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
  3. 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
  4. 重复步骤 3 直到某一指针达到序列尾;
  5. 将另一序列剩下的所有元素直接复制到合并序列尾。

2.4.2 代码

#include <iostream>

using namespace std;


void merge(int* arr, int L, int M, int R)
{
	int* help = new int[R - L + 1];
	int p1 = L;
	int p2 = M+1;
	int i = 0;
	while (p1 <= M && p2 <= R)
	{
		if (arr[p1] <= arr[p2])
			help[i++] = arr[p1++];
		else
			help[i++] = arr[p2++];
	}
	while (p1 <= M)
		help[i++] = arr[p1++];
	while (p2 <= R)
		help[i++] = arr[p2++];
	for (int i = 0; i < (R-L+1); i++)
		arr[L+i] = help[i];
	delete[]help;
}

void mergeSort(int* arr, int L, int R)
{
	if (L == R)
		return;
	int mid = L + ((R - L) >> 1);
	mergeSort(arr, L, mid);
	mergeSort(arr, mid + 1, R);
	merge(arr, L, mid, R);
}


int main()
{
	int arr[5] = { 5,4,3,2,1 };	
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	cout << endl;
	mergeSort(arr, 0, 4);	//注意此时的R应该为4而不是5,否则数组越界报错
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	return 0;
}

2.4.3 复杂度

master公式: T ( N ) = a T ( N b ) + O ( N d ) T(N) = aT(\frac{N}{b})+O(N^d) T(N)=aT(bN)+O(Nd)​ ,

T ( N ) T(N) T(N)​:母问题的规模;

a:子问题被调用的次数;

T ( N b ) T(\frac{N}{b}) T(bN):子问题的规模;

O ( N d ) O(N^d) O(Nd) :除去调用子问题之外的剩下过程的时间复杂度。

l o g b a > d log_ba > d logba>d ,则时间复杂度为: O ( N l o g b a ) O(N^{log_ba}) O(Nlogba)

l o g b a = d log_ba = d logba=d ,则时间复杂度为: O ( N d l o g 2 N ) O(N^dlog_2N) O(Ndlog2N) ;

l o g b a < d log_ba < d logba<d ,则时间复杂度为: O ( N d ) O(N^d) O(Nd)

由master公式,可得: a = 2 ; b = 2 ; d = 1 , l o g b a = d = 1 a = 2;b = 2;d = 1, log_ba=d=1 a=2b=2d=1,logba=d=1 ,因此时间复杂度为: O ( N l o g N ) O(NlogN) O(NlogN)

空间复杂度为: O ( N ) O(N) O(N)​​

2.4.5 归并排序拓展

2.4.5.1 小和问题●

在一个数组中,每一个数的左边比当前数小的数累加起来,叫做这个数组的小和。请求一个数组的小和

例如:[1,3,4,2,5],1左边比1小的数:没有;3左边比3小的数:1;4左边比4小的数:1,3;2左边比2小的数:1;5左边比5小的数:1,3,4,2 。因此该数组的小和为:1+1+3+1+1+3+4+2=16。

方法1:暴力解法
  1. 当来到i位置时,对左边所有的数进行遍历,求出所有比当前数小的数的和,最后累加起来。

  2. 代码:

    #include <iostream>
    
    using namespace std;
    
    void smallTotal(int *arr,int n)
    {
    	int sum = 0;
    	for (int e = 1; e < n; e++)
    	{
    		for (int i = 0; i < e; i++)	
    		{
    			if (arr[i] < arr[e])
    				sum += arr[i];
    		}
    	}
    	cout << sum;
    }
    
    int main()
    {
    	int arr[5] = { 1,3,4,2,5 };
    	for (int i = 0; i < 5; i++)
    		cout << arr[i] << " ";
    	cout << endl;
    	smallTotal(arr, 5);
    	return 0;
    }
    

时间复杂度为: O ( N 2 ) O(N^2) O(N2)

方法2:转换,采用归并●
  1. 把求i位置左边比它小的数的和 转变为 求i位置右边有多少个数比i位置的数大,然后乘上i位置的数。

    ​ 例如:[1,3,4,2,5],1的右边比1大的数的个数:4;3右边比3大的数的个数2;4右边比4大的数的个数:1;2右边比2大的数的个数:1;5右边比5大的数的个数:没有。因此小和为: 4 ∗ 1 + 2 ∗ 3 + 1 ∗ 4 + ∗ 2 = 4 + 6 + 4 + 2 = 16 4*1+2*3+1*4+*2=4+6+4+2=16 41+23+14+2=4+6+4+2=16

  2. 代码:

    #include <iostream>
    
    using namespace std;
    
    int merge(int* arr, int L, int M, int R)	//此处返回值从void变为int
    {
    	int* help = new int[R - L + 1];
    	int p1 = L;
    	int p2 = M + 1;
    	int i = 0;
    	int res = 0;
    	while (p1 <= M && p2 <= R)
    	{
    		if (arr[p1] <= arr[p2])
    		{
    			res += (R - p2 + 1) * arr[p1];	//此处添加求小和的代码:求右边比当前数大的个数并乘以当前数
    			help[i++] = arr[p1++];
    		}
    		else
    		{
    			res += 0;
    			help[i++] = arr[p2++];
    		}
    	}
    	while (p1 <= M)
    		help[i++] = arr[p1++];
    	while (p2 <= R)
    		help[i++] = arr[p2++];
    	for (int i = 0; i < (R - L + 1); i++)
    		arr[L + i] = help[i];
    	delete[]help;
    	return res;								//此处返回小和
    }
    
    int mergeSort(int* arr, int L, int R)		//此处返回值从void变为int
    {
    	if (L == R)
    		return 0;
    	int mid = L + ((R - L) >> 1);
    
    	return  mergeSort(arr, L, mid) +		//左侧排序并求小和的数量
    		mergeSort(arr, mid + 1, R) +		//右侧排序并求小和的数量
    		merge(arr, L, mid, R);				//左侧和右侧一起排序好合并后的小和的数量
    }
    
    int main()
    {
    	int arr[5] = { 1,3,4,2,5 };
    	for (int i = 0; i < 5; i++)
    		cout << arr[i] << " ";
    	cout << endl;
    	cout << "小和:" << mergeSort(arr, 0, 4) << endl;	//注意此时的R应该为4而不是5,否则数组越界报错
    	for (int i = 0; i < 5; i++)
    		cout << arr[i] << " ";
    	return 0;
    }
    

    代码说明:

    ​ 第29-30行的代码十分巧妙,只要判断出左边p1所在位置的数比p2所在位置的数小,那么就可以直接求出右边有 R − p 2 + 1 R - p2 + 1 Rp2+1个数比p1所在位置的数大。

2.4.5.2 逆序对问题●

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

方法:转换,采用归并
  1. 将问题转换成右边有多少个数比左边小

  2. 代码:

    #include <iostream>
    
    using namespace std;
    
    
    void merge(int* arr, int L, int M, int R)
    {
    	int* help = new int[R - L + 1];
    	int p1 = L;
    	int p2 = M + 1;
    	int i = 0;
    	while (p1 <= M && p2 <= R)
    	{
    		if (arr[p1] <= arr[p2])
    			help[i++] = arr[p1++];
    		else
    		{
    			for (int j = p1; j <= M; j++)
    				cout << "{" << arr[j] << "," << arr[p2] << "}" << " ";	//打印逆序对
    			help[i++] = arr[p2++];
    		}
    	}
    	while (p1 <= M)
    		help[i++] = arr[p1++];
    	while (p2 <= R)
    		help[i++] = arr[p2++];
    	for (int i = 0; i < (R - L + 1); i++)
    		arr[L + i] = help[i];
    	delete[]help;
    }
    
    void mergeSort(int* arr, int L, int R)
    {
    	if (L == R)
    		return;
    	int mid = L + ((R - L) >> 1);
    	mergeSort(arr, L, mid);
    	mergeSort(arr, mid + 1, R);
    	merge(arr, L, mid, R);
    }
    
    
    int main()
    {
    	int arr[5] = { 5,4,3,2,1};
    	for (int i = 0; i < 5; i++)
    		cout << arr[i] << " ";
    	cout << endl;
    	mergeSort(arr, 0, 4);	//注意此时的R应该为4而不是5,否则数组越界报错
    	cout << endl;
    	for (int i = 0; i < 5; i++)
    		cout << arr[i] << " ";
    	return 0;
    }
    

    代码说明:

    ​ 18和19行的代码实现打印逆序对的功能。只要右边p2所在位置的数比左边p1所在位置的数小,那么就可以直接循环打印p1到M位置的每个数与p2所在位置的数的逆序对。这一点十分巧妙,如果有疑问,可以自己在草稿纸上画一下示意图。

2.5 快速排序●●

2.5.1 荷兰国旗问题 ●

问题1

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

2.5.1.1 问题一思路

​ 首先划分小于区域“≤区”的左边界为数组第一个元素的左边,然后指定i为0。

​ (1). [ i ] ≤ n u m [i] ≤ num [i]num时: [ i ] [i] [i]和“ ≤ 区 ≤区 ”的下一个数交换,“ ≤ 区 ≤区 ”右扩, i + + i++ i++

​ (2) [ i ] > n u m [i] > num [i]>num时: i + + i++ i++

​ 当i到达数组最右边时停止。

2.5.1.2 问题一代码
#include <iostream>

using namespace std;

void netherlandFlag(int* arr, int n, int num)
{
	int lessOrEqualAreaRightEdge = -1;
	int i = 0;
	while(i<n)
	{
		if (arr[i] <= num)
		{
			int temp = arr[i];
			arr[i] = arr[lessOrEqualAreaRightEdge + 1];
			arr[lessOrEqualAreaRightEdge + 1] = temp;
			lessOrEqualAreaRightEdge++;
			i++;
		}
		else
		{
			i++;
		}
	}
}

int main()
{
	int arr[9] = { 9,8,7,6,5,4,3,2,1 };
	for (int i = 0; i < 9; i++)
		cout << arr[i] << " ";
	cout << endl;
	netherlandFlag(arr, 9, 6);
	cout << endl;
	for (int i = 0; i < 9; i++)
		cout << arr[i] << " ";
	return 0;
}

问题二(荷兰国旗问题)

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

2.5.1.3 问题二思路

​ 首先划分小于区域“ < 区 <区 <”的右边界为数组第一个元素的左边,大于区域“ > 区 >区 >”的左边界为数组最后一个元素的右边,然后指定i为0。

​ (1). [ i ] < n u m [i] < num [i]<num时: [ i ] [i] [i]和“ < 区 <区 <”的下一个数交换,“ < 区 <区 <”右扩, i + + i++ i++

​ (2). [ i ] = n u m [i] = num [i]=num时: i + + i++ i++

​ (3). [ i ] > n u m [i] > num [i]>num时: [ i ] [i] [i]和“ > 区 >区 >”的前一个数交换,“ > 区 >区 >”左扩, i 不变 i不变 i不变

​ 当i与 > 区 >区 >​​的左边界碰上时停止。

2.5.1.3 问题二代码
#include <iostream>

using namespace std;

void netherlandFlag(int* arr, int n, int num)
{
	int lessAreaRightEdge = -1;
	int lagerAreaLeftEage = n;
	int i = 0;
	while (i < lagerAreaLeftEage)
	{
		if (arr[i] < num)
		{
			int temp = arr[i];
			arr[i] = arr[lessAreaRightEdge + 1];
			arr[lessAreaRightEdge + 1] = temp;
			lessAreaRightEdge++;
			i++;
		}
		else if(arr[i] == num)
			i++;
		else if(arr[i] > num)
		{
			int temp = arr[i];
			arr[i] = arr[lagerAreaLeftEage - 1];
			arr[lagerAreaLeftEage - 1] = temp;
			lagerAreaLeftEage--;
		}
	}
}

int main()
{
	int arr[12] = { 5,6,4,7,3,8,2,9,1,3,3,3 };
	for (int i = 0; i < 12; i++)
		cout << arr[i] << " ";
	cout << endl;
	netherlandFlag(arr, 12, 3);
	cout << endl;
	for (int i = 0; i < 12; i++)
		cout << arr[i] << " ";
	return 0;
}

2.5.2 快速排序1.0版本 ●

2.5.2.1 思路

利用荷兰国旗问题一解决,一次搞定一个数,采用最后一个数作为基准(先前荷兰国旗问题的num),

不多需要多做一步的是,最后需要把基准上的数与大于区的第一个数进行交换

这里的大于区是指不包括基准在内以及比基准小的区域。

终止条件是i遍历到R

2.5.2.2 代码
#include <iostream>

using namespace std;

int* partition(int* arr, int L, int R)
{
	int lessOrEqualAreaRightEdge = L - 1;
	int i = L;
	int pos[2] = {0};
	while (i < R)
	{
		if (arr[i] <= arr[R])
		{
			int temp = arr[i];
			arr[i] = arr[lessOrEqualAreaRightEdge + 1];
			arr[lessOrEqualAreaRightEdge + 1] = temp;
			lessOrEqualAreaRightEdge++;
			i++;
		}
		else
			i++;
	}
	int temp = arr[R];
	arr[R] = arr[lessOrEqualAreaRightEdge+1];
	arr[lessOrEqualAreaRightEdge+1] = temp;
	pos[0] = lessOrEqualAreaRightEdge + 1;
	pos[1] = lessOrEqualAreaRightEdge + 2;
	return pos;
}

void quickSort(int* arr, int L, int R)
{
	if (arr == NULL || R - L + 1 < 2)
		return;
	if (L < R)
	{
		int *p = new int[2];
		p = partition(arr, L, R);
		quickSort(arr, L, p[0]-1);
		quickSort(arr, p[1], R);
	}
}

int main()
{
	int arr[5] = { 5,4,3,2,1};
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	cout << endl;
	quickSort(arr, 0, 4);
	cout << endl;
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	return 0;
}

代码说明:

​ 第23到25行表示:把基准上的数与大于区的第一个数进行交换。

​ 最后返回小于等于区的有边界以及大于区的左边界

2.5.2.3 复杂度

时间复杂度: O ( N 2 ) O(N^2) O(N2)

空间复杂度: O ( l o g N ) O(logN) O(logN)

2.5.3 快速排序2.0版本 ●

2.5.3.1 思路

利用荷兰国旗问题二解决,一次搞定一批数(相同的数),2.0版依旧采用最后一个数作为基准(先前荷兰国旗问题的num),

不多需要多做一步的是,最后需要把基准上的数与大于区的第一个数进行交换;

这里的大于区同样是指不包括基准在内的区域。

终止条件是i遍历到大于区

2.5.3.2 代码
#include <iostream>

using namespace std;

int* partition(int* arr, int L, int R)
{
	int lessAreaRightEdge = L - 1;
	int lagerAreaLeftEage = R;
	int i = L;
	int pos[2] = {0};
	while (i < lagerAreaLeftEage)
	{
		if (arr[i] < arr[R])
		{
			int temp = arr[i];
			arr[i] = arr[lessAreaRightEdge + 1];
			arr[lessAreaRightEdge + 1] = temp;
			lessAreaRightEdge++;
			i++;
		}
		else if (arr[i] == arr[R])
			i++;
		else if (arr[i] > arr[R])
		{
			int temp = arr[i];
			arr[i] = arr[lagerAreaLeftEage - 1];
			arr[lagerAreaLeftEage - 1] = temp;
			lagerAreaLeftEage--;
		}
	}
	int temp = arr[lagerAreaLeftEage];
	arr[lagerAreaLeftEage] = arr[R];
	arr[R] = temp;
	pos[0] = lessAreaRightEdge+1;
	pos[1] = lagerAreaLeftEage;
	return pos;
}

void quickSort(int* arr, int L, int R)
{
	if (arr == NULL || R - L + 1 < 2)
		return;
	if (L < R)
	{
		int *p = new int[2];
		p = partition(arr, L, R);
		quickSort(arr, L, p[0]-1);
		quickSort(arr, p[1], R);
	}
}

int main()
{
	int arr[5] = { 5,4,3,2,1};
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	cout << endl;
	quickSort(arr, 0, 4);
	cout << endl;
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	return 0;
}

代码说明:

​ 第31到33行表示:把基准上的数与大于区的第一个数进行交换。

​ 最后返回小于区的右边界以及大于区的左边界。

2.5.3.3 复杂度

时间复杂度: O ( N 2 ) O(N^2) O(N2)​​

空间复杂度: O ( l o g N ) O(logN) O(logN)

2.5.3 快速排序3.0版本 ●●

3.5.3.1 思路

3.0版本与前两个版本不同的是,在数组中随机选取一个数,然后把这个数与数组最后一个数作交换,作为基准,拿它作划分。

3.5.3.2 代码
#include <iostream>
#include <random>

using namespace std;

int* partition(int* arr, int L, int R)
{
	int lessAreaRightEdge = L - 1;
	int lagerAreaLeftEage = R;
	int i = L;
	int pos[2] = { 0 };
	while (i < lagerAreaLeftEage)
	{
		if (arr[i] < arr[R])
		{
			int temp = arr[i];
			arr[i] = arr[lessAreaRightEdge + 1];
			arr[lessAreaRightEdge + 1] = temp;
			lessAreaRightEdge++;
			i++;
		}
		else if (arr[i] == arr[R])
			i++;
		else if (arr[i] > arr[R])
		{
			int temp = arr[i];
			arr[i] = arr[lagerAreaLeftEage - 1];
			arr[lagerAreaLeftEage - 1] = temp;
			lagerAreaLeftEage--;
		}
	}
	int temp = arr[lagerAreaLeftEage];
	arr[lagerAreaLeftEage] = arr[R];
	arr[R] = temp;
	pos[0] = lessAreaRightEdge + 1;
	pos[1] = lagerAreaLeftEage;
	return pos;
}

void quickSort(int* arr, int L, int R)
{
	if (arr == NULL || R - L + 1 < 2)
		return;
	if (L < R)
	{
		std::mt19937 generator;
		std::uniform_int_distribution<int> distribution(0, R-L);
		int randNum = distribution(generator);
		int temp = arr[R];
		arr[R] = arr[L + randNum];
		arr[L + randNum] = temp;
		int* p = new int[2];
		p = partition(arr, L, R);
		quickSort(arr, L, p[0] - 1);
		quickSort(arr, p[1], R);
	}
}

int main()
{
	int arr[5] = { 5,4,3,2,1 };
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	cout << endl;
	quickSort(arr, 0, 4);
	cout << endl;
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	return 0;
}

代码说明:

​ 第46-48行的代码表示:产生一个从0到数组长度减一的随机数,从而在50行时随机选取一个元素与数组最后一个元素做交换后作为基准。

3.5.3.3 复杂度

由于随机,在数学上求期望,最终可以得到时间复杂度为: O ( N l o g N ) O(NlogN) O(NlogN)​​ 。

空间复杂度: O ( l o g N ) O(logN) O(logN)

2.6 堆排序 ●

2.6.1 堆结构

  • 也称为优先队列结构
  • 逻辑概念上为一颗完全二叉树。
2.6.1.1 堆与二叉树
  • 在堆结构中的数组与二叉树:
    • heapsize:数组中连续的一段大小,例如,heapsize=7,则二叉树的范围为0-6
    • i位置的左孩子: 2 ∗ i + 1 2*i+1 2i+1
    • i位置的右孩子: 2 ∗ i + 2 2*i+2 2i+2
    • i位置的父亲: i − 1 2 \cfrac{i-1}{2} 2i1
2.6.1.2 堆结构分类
  • 堆分为大根(顶)堆和小根(顶)堆:
    • 大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列;
    • 小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;
2.6.1.3 构建大根堆
  • 如何构建大根堆?构建大根堆的过程就称为heapinsert过程

    1. 申请一个干净的数组,没有存放任何数据;

    2. 初始化heapsize为0,堆为空;

    3. 此时数组开放接口给用户,用户每来一个数据,根据heapsize的值,则说明新进来的数应该放在heapsieze位置,然后heapsize+1,例如,heapsize为0,则数据放在数组的0位置;

    4. 重复步骤3,若发现当前的堆不是大根堆,即当前节点发现自己的值比父节点要大的时候,则当前节点与父节点进行交换;

      1. 如何发现不是大根堆?

        答:根据坐标变换。

  • 代码:

    //某个数在index位置,往上继续移动
    void heapInsert(int* arr, int index)
    {
    	while (arr[index] > arr[(index - 1)/2])	//当前数大于父位置的数,则交换
    	{
    		int temp = arr[index];
    		arr[index] = arr[(index - 1) / 2];
    		arr[(index - 1) / 2] = temp;
    		index = (index - 1) / 2;
    	}
    }
    
  • 复杂度

    时间复杂度: O ( l o g N ) O(logN) O(logN)

2.6.1.4 获取(返回)大根堆的最大值
  • 原则:将最大值返回后,依然要确保剩下的堆为大根堆。

  • 操作:

    1. 返回0位置上的数,然后将最后的一个数放在0位置上,再然后heapsize--
    2. 在当前节点的左右孩子中选取一个最大值,然后与当前节点进行pk,若pk失败,则与较大的孩子作交换。(这个过程称为heapify:即“堆化”的过程)
    3. 重复步骤2,当子节点都没有比当前节点数值大时停止,或者以及没有子节点时停止。
  • 代码:

    //某个数在index位置,是否往下移动;由heapSize管理堆的大小
    void heapify(int* arr, int index, int heapSize)
    {
    	int left = 2 * index + 1;	//左孩子下标
    	while (left < heapSize)		//当下方还有孩子的时候
    	{
    		int largeIndex = 0;
    		if (left + 1 < heapSize)	//如果有右边孩子
    		{
    			if (arr[left + 1] > arr[left])	//两个孩子中谁的值大,就把谁的下标传给largeIndex。
    				largeIndex = left + 1;
    			else if (arr[left + 1] < arr[left])
    				largeIndex = left;
    		}
    		else    //没有右孩子,则把左孩子的下标传给largeIndex
    			largeIndex = left;
    		if (arr[largeIndex] > arr[index])	//孩子与父亲谁的值大,就把谁的下标传给largeIndex
    			largeIndex = largeIndex;
    		else
    			largeIndex = index;
    		if (largeIndex == index)	//说明孩子没有干过爹,则退出循环
    			break;
    		//若干过了爹,则孩子当爸爸,爸爸当儿子
    		int temp = arr[index];
    		arr[index] = arr[largeIndex];
    		arr[largeIndex] = temp;
    		//更新子节点
    		left = 2 * index + 1;
    	}
    }
    
  • 复杂度

    时间复杂度: O ( l o g N ) O(logN) O(logN)

2.6.2 堆排序

2.6.2.1 思路
  1. 根据原始数组创建大根堆 H[0……n-1];(heapinsert
  2. 把堆首(最大值)和堆尾互换,heapsize--
  3. 把堆的尺寸缩小 1,并调用 heapify,目的是把新的数组顶端数据调整到相应位置;(heapify
  4. 重复步骤 2,直到堆的尺寸为 1。
2.6.2.2 代码
void heapSort(int* arr, int n)
{
	if (arr == NULL || n < 2)
		return;
#if 1	//方法一
	for (int i = 0; i < n; i++)	// O(N)
		heapInsert(arr, i);	// O(logN)
#elif 0	//方法2
	for (int i = n-1; i >= 0; i--)	// O(N)
		heapify(arr, i, n);
#endif

	int heapSize = n;
	int temp = arr[heapSize-1];
	arr[--heapSize] = arr[0];
	arr[0] = temp;
	while (heapSize > 0)	// O(N)
	{
		heapify(arr, 0, heapSize);	// O(logN)
		int temp = arr[heapSize - 1];	// O(1)
		arr[--heapSize] = arr[0];			
		arr[0] = temp;
	}
}

int main()
{
	int arr[5] = { 5,4,3,2,1 };
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	cout << endl;
	heapSort(arr, 5);
	cout << endl;
	for (int i = 0; i < 5; i++)
		cout << arr[i] << " ";
	cout << endl;
	return 0;
}

代码说明:

​ 第5-7行做heapInsert,构造大根堆。也可以采用第二种写法,如8-10行代码所示,从最末尾的节点开始进行heapify,一直到根节点为止。

2.6.2.3 复杂度

时间复杂度为: O ( N l o g N ) O(NlogN) O(NlogN)

空间复杂度为: O ( 1 ) O(1) O(1)​ ;

2.6.3 堆排序扩展

已知一个几乎有序的数组(几乎有序是指如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小)。请选择一个合适的算法对这个数据进行排序。

2.6.3.1 思路:

假设k = 6,准备一个小根堆,先遍历数组的前7个位置(0-6)上的数,将这7个数放入小根堆,那么在排完序后,小根堆的最小值一定在0的位置。因为在排完序后,从索引7开始以后的数不可能在0位置上,这是题目要求写死了的。

换句话说,就是索引7位置的数,最多只能移动到索引1的位置(最多移动k=6个位置),所以在前7个数的小根堆中,小根堆的最小值一定在索引0的位置上。

接下来,弹出小根堆的堆顶(最小值),放到索引0的位置上,再把索引7位置上的数放入小根堆,然后这个新的小根堆的最小值就应该放到索引1的位置上,周而复始。

最后,数组临近结束的时候,依次弹出剩余小根堆的每一个最小值,整个数组便有序了。

2.6.3.2 代码(使用vector和queue版本)
#include <iostream>
#include <vector>
#include <queue>

using namespace std;

void sortNearlySortedArray(vector<int>& arr, int k) {
    // 创建一个最小堆
    priority_queue<int, vector<int>, greater<int>> minHeap;

    // 将数组的前 k+1 个元素加入到最小堆
    for (int i = 0; i <= k && i < arr.size(); ++i) {
        minHeap.push(arr[i]);
    }

    int index = 0;
    // 处理剩余元素
    for (int i = k + 1; i < arr.size(); ++i) {
        arr[index++] = minHeap.top();
        minHeap.pop();
        minHeap.push(arr[i]);
    }

    // 处理剩下的堆中元素
    while (!minHeap.empty()) {
        arr[index++] = minHeap.top();
        minHeap.pop();
    }
}

int main() {
    vector<int> arr = { 3, 2, 1, 5, 4, 7, 6, 5, 8 };
    int k = 2; // 设置每个元素最多可以移动2个位置

    sortNearlySortedArray(arr, k);

    for (int i = 0; i < arr.size(); ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;

    return 0;
}

2.6.3.2 代码(不使用vector和queue版本)

如果不使用 STL 提供的 vectorpriority_queue,可以手动实现一个最小堆。以下是使用数组来实现最小堆,并进行排序的代码:

#include <iostream>

using namespace std;

void heapify(int heap[], int n, int i) {
    int smallest = i;
    int left = 2 * i + 1;
    int right = 2 * i + 2;

    if (left < n && heap[left] < heap[smallest])
        smallest = left;

    if (right < n && heap[right] < heap[smallest])
        smallest = right;

    if (smallest != i) {
        swap(heap[i], heap[smallest]);
        heapify(heap, n, smallest);
    }
}

void buildHeap(int heap[], int n) {
    for (int i = n / 2 - 1; i >= 0; --i) {
        heapify(heap, n, i);
    }
}

void sortNearlySortedArray(int arr[], int n, int k) {
    int heapSize = k + 1;
    int* heap = new int[heapSize];

    // Build the initial heap from the first k+1 elements
    for (int i = 0; i < heapSize; ++i) {
        heap[i] = arr[i];
    }

    buildHeap(heap, heapSize);

    int index = 0;
    for (int i = heapSize; i < n; ++i) {
        arr[index++] = heap[0];
        heap[0] = arr[i];
        heapify(heap, heapSize, 0);
    }

    while (heapSize > 0) {
        arr[index++] = heap[0];
        heap[0] = heap[--heapSize];
        heapify(heap, heapSize, 0);
    }

    delete[] heap;
}

int main() {
    int arr[] = {3, 2, 1, 5, 4, 7, 6, 5, 8};
    int n = sizeof(arr) / sizeof(arr[0]);
    int k = 2; // 每个元素最多可以移动2个位置

    sortNearlySortedArray(arr, n, k);

    for (int i = 0; i < n; ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;

    return 0;
}

代码解释

  1. heapify 函数: 这是一个辅助函数,用于维护最小堆的性质。给定一个堆数组 heap,以及一个索引 i,此函数确保以 i 为根的子树满足最小堆性质。如果发现任何子节点小于 heap[i],则交换它们并递归地调用 heapify
  2. buildHeap 函数: 从一个无序数组构建最小堆。这个函数从最后一个非叶子节点开始,对每个节点调用 heapify 以建立最小堆。
  3. sortNearlySortedArray 函数:
    • 首先创建一个大小为 k + 1 的最小堆,并用数组的前 k + 1 个元素来初始化它。
    • 使用 buildHeap 函数构建最小堆。
    • 依次从数组的剩余部分取出元素,放入堆中,并将堆的最小元素放入数组的正确位置。
    • 最后,将堆中剩余的元素依次放入数组中,完成排序。
  4. main 函数:
    • 初始化一个几乎有序的数组 arr
    • 调用 sortNearlySortedArray 对数组进行排序。
    • 输出排序后的数组。
2.6.3.4 复杂度

时间复杂度: O ( N l o g K ) O(NlogK) O(NlogK) ,当K很小时,可进化成 O ( N ) O(N) O(N)​ 。

2.7 计数排序

2.7.1 原理

将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

当输入的元素是 n 个 0 到 k 之间的整数时,它的运行时间是 Θ(n + k)。计数排序不是比较排序,排序的速度快于任何比较排序算法。

由于用来计数的数组C的长度取决于待排序数组中数据的范围(等于待排序数组的最大值与最小值的差加上1),这使得计数排序对于数据范围很大的数组,需要大量时间和内存。例如:计数排序是用来排序0到100之间的数字的最好的算法,但是它不适合按字母顺序排序人名。但是,计数排序可以用在基数排序中的算法来排序数据范围很大的数组。

2.7.2 步骤

(1)找出待排序的数组中最大和最小的元素

(2)统计数组中每个值为i的元素出现的次数,存入数组C的第i项

(3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加)

(4)反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

2.7.2代码

(未完待续)
  • 23
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值