数据结构与算法(算法篇)

学数据结构与算法不是仅仅学算法本身(经验),而是学习思维(解决问题的能力)。

1、算法的性能分析

衡量指标:时间复杂度、空间复杂度、稳定性

1.1 时间复杂度

用O()表示,本质就是:算法的计算次数。

O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < …

#include<iostream>
using namespace std;

void test01()
{
	// 时间复杂度:O(n)
	int n, ans = 0;
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		ans++;
	}


	// 有效计算次数 / 实际时间复杂度:O(n+3)
	// 但当运行次数趋于无穷时候,3可以忽略不计
	// 时间复杂度:O(n)
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		ans++;
	}
	ans++;
	ans++;
	ans++;
}


void test02()
{
	// 有效计算次数 / 实际时间复杂度:O(n/2)
	// 但当运行次数趋于无穷时候,1/2可以忽略不计
	// 时间复杂度:O(n)
	int n, ans = 0;
	cin >> n;
	for (int i = 0; i < n; i+=2)
	{
		ans++;
	}
}

void test02()
{
	// 有效计算次数 / 实际时间复杂度:O(n/2)
	// 时间复杂度:O(n)
	int n, ans = 0;
	cin >> n;
	for (int i = 0; i < n; i += 2)
	{
		ans++;
	}
}

void test03()
{
	int n, ans = 0;
	cin >> n;
	// 时间复杂度:O(n^2)
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			ans++;
		}
	}


	// 有效计算次数 / 实际时间复杂度:O(n^2+n)
	// 但当运行次数趋于无穷时候,n相较于n^2,可以忽略不计
	// 时间复杂度:O(n^2)
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			ans++;
		}
	}
	for (int i = 0; i < n; i++)
	{
		ans++;
	}
}

int main()
{
	test01();
	system("pause");
	return 0;
}

1.2 空间复杂度

本质就是:除去输入数据本身所占用的空间之外,额外产生的空间(不是数据本身);这些数据通常是由用户提供的或从外部系统读取的,是算法的“输入”。
额外:也就是算法为了处理数据,额外使用的空间。
空间复杂度是和内存有关。
logn的空间比较少,n^2这种空间的嵌套也比较少。

O(1) < O(n) < O(nlogn)

int x = 5; 定义了一个整型变量 x,它占用的空间是固定的,不随输入规模 n 变化,因此其空间复杂度为 O(1)。
int arr[n]; 定义了一个大小为 n 的数组 arr,需要 n 个整型空间。因此,arr 占用的空间随 n 增长,空间复杂度为 O(n)。

void example(int n) 
{
    int x = 5;  // 占用常数空间 O(1)
    int arr[n]; // 占用线性空间 O(n)
}

假设你有一个算法,它需要对一个整数数组进行排序。这个数组就是输入数据。输入数据:arr[] 是输入数据,它是算法需要处理的内容。arr 的大小是 n,所以输入数据本身占用的空间是 O(n)。那么这个部分我们通常不考虑在空间复杂度分析中,因为这是算法必须使用的空间。

void sortArray(int arr[], int n) 
{
    // 对数组进行排序的算法
}

额外空间:这是除了存储输入数据之外,算法需要额外分配的空间。例如,创建新的数组、栈空间(在递归中)或者额外的变量。

void processData(int arr[], int n) 
{
    int temp;           // 一个额外的变量,占用常数空间 O(1)
    int result[n];      // 一个新的数组,占用 O(n) 的空间
    
    for (int i = 0; i < n; i++) {
        result[i] = arr[i] * 2;  // 操作输入数据,并存储到新数组中
    }
}

上述示例中:
输入数据:arr[] 是输入数据,它的大小为 n,输入数据本身的空间复杂度是 O(n)。
额外空间:变量 temp 占用了 O(1) 的空间,因为它是一个单独的整数变量。
数组 result[] 占用了 O(n) 的空间,因为它的大小与输入数组 arr[] 相同。

#include<iostream>
using namespace std;

void test01()
{
	// 冒泡排序
	// 真实时间复杂度:O((n^2-n) / 2 + 2n)
	// 时间复杂度:O(n^2)
	// 空间复杂度:O(1)
	int a[101];
	int n;
	cin >> n;
	// 这里的a占用n个空间,属于输入,不算额外产生的空间
	for (int i = 0; i < n; i++)
	{
		cin >> a[i];
	}

	for (int i = n - 1; i > 0; i--)
	{
		for (int j = 0; j < n - 1; j++)
		{
			if (a[j] > a[j + 1])
			{
				// swap底层其实是有第三变量temp交换,但是是有穷的,算O(1)
				swap(a[j], a[j + 1]);
			}
		}
	}

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

void test02()
{
	int n;
	cin >> n;
	// 使用了额外的空间,空间复杂度:0(n)
	int* arr = new int[n];	
	for (int i = n - 1; i > 0; i--)
	{
		for (int j = 0; j < n - 1; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				swap(arr[j], arr[j + 1]);
			}
		}
	}

	delete[] arr;
}

int main()
{
	test01();
	system("pause");
	return 0;
}

1.3 小结

时间复杂度越高,程序运行的时间就越长。
时间和空间往往是相对的,一般,可能会用时间换空间,用空间换时间。

2、高精度

为什么要接触高精度?
主要就是:解决数据溢出的问题。
下方的代码,不同的整型是有取值范围的。

int main()
{
	// int型就是:-2^31 到 2^32-1
	// long long 是:-2^63 到 2^63-1
	int a, b;
	cin >> a >> b;
	cout << a + b << endl;
	system("pause");
	return 0;
}

还没等输入第二个变量的赋值,程序就结束了。低精度,就搞不定我们这种需求了。

在这里插入图片描述
高精度的本质就是使用字符进行计算。
但是,当发现使用字符串(字符数组)时,能够很好地解决这种数据溢出的问题,但是字符串之间不能够像数值计算那样,+号运算符重载也只是字符串拼接。

int main()
{
	string a, b;
	cin >> a >> b;
	cout << a + b << endl;
	system("pause");
	return 0;
}

在这里插入图片描述
但是,单个字符使用ASCII码,进行存储的,字符’1’是49,使用字符'1' - '0'就可以巧妙的得到数值1本身。

2.1 高精度加法

注意事项:
1、字符数组局部变量要初始化,可以写到全局变量(默认初始化为0)
2、高精度的本质就是使用字符进行计算,通过字符 - ‘0’ 转换为数值本身
3、使用两个字符数组相加,存放结果的数组的长度为这两个字符数组中的最长长度+1

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

void string_to_int(string str, int dst[])
{
	for (int i = 0; i < str.size(); i++)
	{
		// "1234"
		// 要放置成
		// "4321"
		// 也就是第0个放在第3个,第1个放在第2个,
		// 如果有n个,第i个就是放在第n-i-1个
		dst[str.size() - i -1] = str[i] - '0';
	}
}


int main()
{
	//  "1234"
	// +"567"  
	// 个位不能够有效对齐,所以对字符数组取反,再计算
	//  "4321"
	// +"7650" 
	//  "1081"
	// 取反不影响计算,最后算完,再反转,得:1801就可以了。

	
	string s1, s2;
	// 字符数组局部变量要初始化,可以写到全局变量(默认初始化为0)
	int a[101]= { 0 };
	int b[101]= { 0 };
	int c[101]= { 0 };

	cin >> s1 >> s2;
	// 1.个位对齐(先反转),字符转整型
	string_to_int(s1, a);
	string_to_int(s2, b);
	for (int i = 0; i < s1.size(); i++)
	{
		cout << a[i];
	}
	cout << endl;
	for (int i = 0; i < s2.size(); i++)
	{
		cout << b[i];
	}
	cout << endl;

	// 2.对位相加,放到c数组中,但首先要计算c数组的长度
	// "9999" + "1"  是5位。所以两数相加,结果的最高的长度就是最长的数位数+1
	int c_len = max(s1.size(), s2.size()) + 1;

	// 3.对位相加,放到c数组中
	for (int i = 0; i < c_len; i++)
	{
		c[i] += a[i] + b[i];

		// 下一位进位
		c[i + 1] = c[i] / 10;

		// 两数相加可能会有进位,保留个位
		c[i] = c[i] % 10;
	}
		
		

	// 反转输出,比如相加为1801没有进位, 但是这样可能会造成c_len多1个0
	// 就会是18010
	// 从最后1位逐个判断首位是否是0,逐个去0
	while (c[c_len] == 0 && c_len>1)
	{
		c_len--;
	}
	// 倒叙打印
	for (int i = c_len; i >= 0; i--)
	{
		cout << c[i];
	}
	cout << endl;
	system("pause");
	return 0;
}

2.2 高精度减法

注意事项:
1、最好保证是的数 - 的数,最后加个负号就行;长度相同,就比字符串。
2、使用两个字符数组相减,存放结果的数组的长度为这两个字符数组中的最长长度
3、在字典序中,比较两个字符串时是逐字符进行的,每个字符的排序取决于其编码值。即使 s1 和 s2 的长度不同,只要 s1 的第一个字符在字典序中大于 s2 的第一个字符,结果就是 true。

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

void string_to_int(string str, int dst[])
{
	for (int i = 0; i < str.size(); i++)
	{
		// "1234"
		// 要放置成
		// "4321"
		// 也就是第0个放在第3个,第1个放在第2个,
		// 如果有n个,第i个就是放在第n-i-1个
		dst[str.size() - i -1] = str[i] - '0';
	}
}

bool comStr(string str1, string str2)
{
	if (str1.size() != str2.size())
	{
		return str1.size() >= str2.size();
	}
	else
	{
		// 两个字符串长度相同,比大小
		return str1 >= str2;
	}
}

int main()
{
	int a[101] = { 0 };
	int b[101] = { 0 };
	int c[101] = { 0 };

	string s1, s2;
	cin >> s1 >> s2;
	if (comStr(s1, s2) == false)
	{
		// 如果s1比s2长度短,或s1 < s2,进行替换,这样就能保证大数减小数
		/*string temp = s1;
		s1 = s2;
		s2 = temp;*/
		swap(s1, s2);
	}

	// 1.反转
	string_to_int(s1, a);
	string_to_int(s2, b);

	// 2.对位相减,放到c数组中,但首先要计算c数组的长度
	// "9999" - "1"  是4位。所以两数相减,结果的最高的长度就是最长的数位数
	int c_len = max(s1.size(), s2.size());

	// 3.对位相减,放到c数组中
	//  "1234"
	//- "567"
	// 反转
	//   "4321"
	// - "765" 
	for (int i = 0; i < c_len; i++)
	{
		// 要判断个位够不够减
		if (a[i] < b[i])
		{
			a[i] += 10;  //借位
			a[i + 1]--;  // 高位减1
		}
		c[i] = a[i] - b[i];
	}
	while (c[c_len] == 0 && c_len > 0)
	{
		c_len--;
	}
	// 倒叙打印
	for (int i = c_len; i >= 0; i--)
	{
		cout << c[i];
	}
	cout << endl;
	system("pause");
	return 0;
}

2.3 高精度乘法

注意事项:
1、乘法里还是有加法的影子,找通项公式
2、使用两个字符数组相乘,存放结果的数组的长度最大为这两个字符数组中的长度之和
3、在字典序中,比较两个字符串时是逐字符进行的,每个字符的排序取决于其编码值。即使 s1 和 s2 的长度不同,只要 s1 的第一个字符在字典序中大于 s2 的第一个字符,结果就是 true。

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

void string_to_int(string str, int* a)
{
	for (int i = 0; i < str.size(); i++)
	{
		// "1234"
		// 要放置成
		// "4321"
		// 也就是第0个放在第3个,第1个放在第2个,
		// 如果有n个,第i个就是放在第n-i-1个
		a[str.size() - i - 1] = str[i] - '0';
	}
}

int main()
{
	int a[201] = { 0 };
	int b[201] = { 0 };
	int c[201] = { 0 };

	string s1, s2;
	cin >> s1 >> s2;

	// 1.反转
	string_to_int(s1, a);
	string_to_int(s2, b);

	// 2.对位相乘,放到c数组中,但首先要计算c数组的长度
	// "1000" * "10"  是5位。所以两数相乘,结果的最高的长度就是最长的数位数
	int c_len = s1.size() + s2.size();

	// 3.对位相乘,放到c数组中
	//                 a3         a2        a1        a0
	//*                                     b1        b0
	//                 a3b0       a2b0     a1b0       a0b0
	//+       a3b1     a2b1       a1b1     a0b1
	// 
 	//对应     c[4]     c[3]     c[2]       c[1]        c[0]
 	//结合a和b的下标,规律就是:c[i+j] = a[i] + b[j]
	//      c[3+1]   c[3+0]  c[2+0]  c[1+0]   c[0+0]
	for (int i = 0; i < s1.size(); i++)
	{
		for (int j = 0; j < s2.size(); j++)
		{
			// 这里应该是累加
			// 比如:这里c[2] = a3b1 + a2b2  两项
			c[i + j] += a[i] * b[j];

			// 进位,也是累加,可能是多次进位
			c[i + j + 1] += c[i + j] / 10;
			
			// 留下个位
			c[i + j] = c[i + j] % 10;
			
		}
	}
	while (c[c_len] == 0 && c_len > 1)
	{
		c_len--;
	}
	// 倒叙打印
	for (int i = c_len; i >= 0; i--)
	{
		cout << c[i];
	}
	cout << endl;
	system("pause");
	return 0;
}

2.4 高精度除法

注意事项:
1、不用字符串取反
2、只用考虑够除余数就行。

#include<iostream>
using namespace std;


void string_to_int(string str, int* a)
{
	for (int i = 0; i < str.size(); i++)
	{
		// 将每个字符转换成数值
		a[i] = str[i] - '0';
	}
}
	

// 高精度除法(高精度 / 低精度)
int main()
{
	string s;
	int a[201] = { 0 };  // 高精度
	long long b;  // 低精度
	int c[201] = { 0 };  // 存放结果(高精度)

	int temp = 0;

	cin >> s;
	cin >> b;
	string_to_int(s, a);
	for (int i = 0; i < s.size(); i++)
	{
		c[i] = (temp * 10 + a[i]) / b;
		// 余数
		temp = (temp * 10 + a[i]) % b;
	}

	int lc = 0;
	while (c[lc] == 0 && lc < s.size())
	{
		lc++;
	}
	for (int i = 0; i < s.size(); i++)
	{
		cout << c[i];
	}
	cout << endl;
	system("pause");
	return 0;
}

3、排序

学排序的根本不是将一个数组排序成功,那其实1个sort就可以完成了。而是掌握其中的思维
由于涉及多个排序算法,采用分文件编写。
.h头文件:

#pragma once
#include<iostream>
using namespace std;
#include<vector>
#include<algorithm>
class mySort
{
public:
	// 打印数组
	void printArr(vector<int>& v);
	// 冒泡排序
	void BubbleSort(vector<int>& v);
	// 选择排序
	void SelectSort(vector<int>& v);
	// 插入排序
	void InsertSort(vector<int>& v);
	// 希尔插入排序
	void ShellInsertSort(vector<int>& v, int start, int gap);
	// 希尔排序
	void ShellSort(vector<int>& v);
	// 计数排序
	void CountSort(vector<int>& v);
	// 桶排序
	void BuckerSort(vector<int>& v);
	// 构建大根堆
	void Build_max_headp(vector<int>& v, int start, int end);
	// 堆排序
	void HeadpSort(vector<int>& v);
	// 快速排序
	void QuickSort(vector<int>& v, int l, int r);
	// 递归分解
	void Merge_up2down(vector<int>& v, int l, int r);
	// 归并排序
	void MergeSort(vector<int>& v, int l, int mid, int r);
};

3.1 冒泡排序

在这里插入图片描述

想法:
1、就是先写完第一趟排序,再套个外层循环全部遍历完。
2、第一躺比n-1次,第二趟比n-2次。。。。
注意事项:
写完任何一个算法,必须对该算法进行评价时间复杂度空间复杂度稳定性

#include"MySort.h"

// 打印数组
void mySort::printArr(vector<int>& v)
{
	// auto 是 C++11 引入的关键字,用于自动推导变量的类型。
	for (auto& i : v)
	{
		cout << i << " ";
	}
	cout << endl;
}

// 冒泡排序
void mySort::BubbleSort(vector<int>& v)
{
	for (int j = v.size() - 1; j >=1; j--)
	{
		// 先写完第一趟排序,第一躺比n-1次,第二趟比n-2次。。。。
		// n-1,n-2 一直递减 所以将这个设置成参数j;最后只剩两个数相比,也就是v[0] v[0+1] 所以i最后<1结束
		//for (int i = 0; i < v.size()-1; i++)
		// 其实循环就是找初始值和结束条件的值
		for (int i = 0; i < j; i++)
		{
			if (v[i] > v[i + 1])
			{
				swap(v[i], v[i + 1]);
			}
	}
	
	}
}

但是,当在某一趟已经排序完成,外层循环仍然会继续遍历,增加了时间复杂度。因此,可以进行优化,当某一趟排序完成,则结束程序。

// 冒泡排序
// 时间复杂度:O(n^2)
// 空间复杂度(额外):O(1)
// 稳定性:稳定(判断一个算法是否稳定的方法就是:看一个数组中两个相同元素的相对位置是否会发生改变)
void mySort::BubbleSort(vector<int>& v)
{
	for (int j = v.size() - 1; j >=1; j--)
	{
		// 定义1个flag
		bool flag = false;
		for (int i = 0; i < j; i++)
		{
			if (v[i] > v[i + 1])
			{
				swap(v[i], v[i + 1]);

				// 发生swap,说明还没排序结束
				flag = true;
			}

		}
		// 这一趟结束还是false,说明排序完成,结束程序。
		if (flag == false)
		{
			break;
		}
	
	}
}

3.2 选择排序

选择排序是冒泡排序的优化,冒泡排序是将所有的比较完,选择排序是首先选择一个最值。
第一趟:假设20是i,假设是最值,那么40就是i+1,是j,j++就是40及以后的数都要和i比
当10 < 20时,则需要更新最值。
在这里插入图片描述

// 选择排序
// 时间复杂度:O(n^2)
// 空间复杂度(额外):O(1)
// 稳定性:不稳定
void mySort::SelectSort(vector<int>& v)
{
	for (int i = 0; i < v.size(); i++)
	{
		int min_index = i;
		// j从i的下一个数出发
		for (int j = i + 1; j < v.size(); j++)
		{
			if (v[j] < v[min_index])
			{
				min_index = j;
			}
		}
		// 整个遍历完,找到1个min_index,和i进行交换
		swap(v[i], v[min_index]);

	}
}

3.3 插入排序

假设20是i,是有序区,那么30往后就是无序区,是j。
当30 > 20时,不用插入,直接j++就行。一旦,10 < 20时,10存放到临时temp,20、30、40位移++,10插入到i位置。
j在外层,因为每个无序区的元素,都得遍历一次有序区,i在内层,
在这里插入图片描述

//插入排序
// 时间复杂度:O(n^2)
// 空间复杂度(额外):O(1)
// 稳定性:稳定
void mySort::InsertSort(vector<int>& v)
{
	// 无序区下表首先从1开始
	for (int j = 1; j < v.size(); j++)  //构造无序区
	{
		// 有序区下表首先从0开始,一直到无序区第一个元素的前一个
		for (int i = 0; i < j; i++)  //构造有序区
		{
			if (v[j] < v[i])
			{
				int temp = v[j];
				// 这一层循环不是每次都执行,是有条件才执行的,时间是有穷的,是O(1)
				for (int k = j-1; k >= i; k--)
				{
					v[k + 1] = v[k];
				}
				v[i] = temp;
				break; // 这一趟已经结束,不用再遍历
			}
		}
	}
}

3.4 希尔排序

希尔排序(Shell Sort)是插入排序的一种,它是针对直接插入排序算法的改进。该方法又称缩小增量排序,因DL. Shell于1959年提出而得名。
希尔排序实质上是一种分组插入方法。它的基本思想是:对于n个待排序的数列,取一个小于n的整数gap(gap被称为步长)将待排序元素分成gap个组子序列,所有距离为gap的倍数的记录放在同一个组中;然后,对各组内的元素进行直接插入排序。这一趟排序完成之后,每一个组的元素都是有序的。然后减小gap的值,并重复执行上述的分组和排序。重复这样的操作,当gap=1时,整个数列就是有序的。
分组排序只是针对局部的某些元素进行排序,分组插入排序后就是会使得这些元素变得相对有序。最后,将整个1组元素变得相对有序之后,再进行一个插入排序,此时的效率是直接对1个无序序列进行插入排序的0.几倍之上
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// 希尔插入排序
void mySort::ShellInsertSort(vector<int>& v, int start, int gap)
{
	// 无序区下表首先从1开始
	for (int j = start + gap; j < v.size(); j+=gap)  //构造无序区
	{
		// 有序区下表首先从0开始,一直到无序区第一个元素的前一个
		for (int i = start; i < j; i+=gap)  //构造有序区
		{
			if (v[j] < v[i])
			{
				int temp = v[j];
				// 这一层循环不是每次都执行,是有条件才执行的,时间是有穷的,是O(1)
				for (int k = j - gap; k >= i; k-=gap)
				{
					v[k + gap] = v[k];
				}
				v[i] = temp;
				break; // 这一趟已经结束,不用再遍历
			}
		}
	}
}

// 希尔排序
// 时间复杂度:O(n^2)
// 空间复杂度(额外):O(1)
// 稳定性:不稳定(局部的组和组之间的位置发生了相对变化)
void mySort::ShellSort(vector<int>& v)
{
	// 首先是4组,然后是2组,每次 ÷ 2,最后1次是1组
	for (int gap = v.size() / 2; gap >= 1; gap/=2)
	{
		for (int i = 0; i < gap; i++)
		{
			// 首先下表是从0开始的组,然受是1开始的组,下标也得套个循环
			ShellInsertSort(v, i, gap);

		}
	}
}

3.5 计数排序

在这里插入图片描述

// 计数排序
void mySort::CountSort(vector<int>& v)
{
	// 1.先找最大值
	int max_value = *max_element(v.begin(), v.end());
	
	// 2.根据最大值开辟数组
	int* countArr = new int[max_value + 1];
	// 用于将指定的内存块设置为某个值,一共(max_value + 1)个int型指针
	memset(countArr, 0, (max_value + 1) * sizeof(countArr));

	// 3.将数组元素放入到新数组中并计数
	// c++ 11风格
	/*for (auto& i : v) 
	{
		countArr[i]++; 
	}*/
	for (int i = 0; i < v.size(); i++)
	{
		countArr[v[i]]++;
	}
	
	// 4.按照下标依次取出新数组中的元素
	v.clear();  // 原数组进行清空再更新
	for (int i = 0; i < max_value + 1; i++)
	{
		while (countArr[i])
		{
			v.push_back(i);
			countArr[i]--;
		}
	}
}

3.6 桶排序

桶排序是计数排序的扩展版本,计数排序可以看成每个桶只存储相同元素,而桶排序每个桶存储一定范围的元素,通过映射函数,将待排序数组中的元素映射到各个对应的桶中,对每个桶中的元素进行排序,最后将非空桶中的元素逐个放入原序列中。
桶排序需要尽量保证元素分散均匀,否则当所有数据集中在同一个桶中时,桶排序失效
在这里插入图片描述

// 桶排序
// 时间复杂度:n+m(桶的数量)*n/m*logn/m = n+n*logn/m = n+n*(logn-logm) = O(N+C)(如果是冒泡,C就是别的复杂度了)
// 空间复杂度(额外):O(M+N) N:桶空间,M:排序空间
// 稳定性:稳定性取决于所选择的排序
void mySort::BuckerSort(vector<int>& v)
{
	// 1.先找最大值、最小值
	int max_value = *max_element(v.begin(), v.end());  // 时间复杂度:n
	int min_value = *min_element(v.begin(), v.end());  // 时间复杂度:n

	// 2.确定桶的数量
	// 通过最大值和最小值来确定,最多一共(max_value - min_value + 1)个数
	// 极端情况下,可能所有数放在一个桶里,也就是1个桶要能容纳n个数
	int bucketNum = (max_value - min_value + 1) / v.size() + 1;  // 要加1,下标从0开始

	// 每个桶里还得存放1维数组
	vector<vector<int>> Buckets(bucketNum);

	// 遍历原始数据
	for (int i = 0; i < v.size(); i++)  // 时间复杂度:n
	{
		// 确定当前元素在第几个桶的索引,也就是当前元素距离最小元素的距离 ÷ 每个桶存放的元素个数
		int index = (v[i] - min_value + 1) / v.size();
		Buckets[index].push_back(v[i]);
	}

	for (int i = 0; i < Buckets.size(); i++)  // 时间复杂度:m
	{
		// 桶排序原理就是:分好桶后,每个桶的排序可以采用多种排序算法
		sort(Buckets[i].begin(), Buckets[i].end());  // 快排    // 时间复杂度:n/m*logn/m
	}
	 
	v.clear();
	for (int i = 0; i < Buckets.size(); i++)
	{
		for (int j = 0; j < Buckets[i].size(); j++)
		{
			// 取出第i个桶里第j个元素
			v.push_back(Buckets[i][j]);
		}
	}
}

3.7 堆排序

堆排序(Heap Sort)是指利用这种数据结构所设计的一种排序算法。
因此,学习堆排序之前,有必要了解堆!若读者不熟悉堆,建议先了解堆(健议可以通过二叉堆,左倾堆,斜堆,二项堆或斐波那契堆等文章进行了解)。
我们知道,堆分为"最大堆""最小堆"。最大堆通常被用来进行"升序"排序而最小堆通常被用来进行"降序"排序。
最大堆进行升序排序的基本思想
①初始化堆:将数列a[1…n]构造成最大堆
②交换数据:将a[1]和a[0]交换,使a[n]是al[1…n]中的最大值;然后将al[1…n-1]重新调整为最大堆。接着,将a[1]和a[n-1]交换,使a[n-1]是a[1…n-1]中的最大值;然后将a[1…n-2]重新调整为最大值。依次类推,直到整个数列都是有序的。
下面,通过图文来解析堆排序的实现过程。注意实现中用到了"数组实现的二叉堆的性质"。在第一个元素的索引为0的情形中:
性质一:索引为i的左孩子的索引是(2i+1);
性质二:索引为i的左孩子的索引是(2
i+2);
性质三:索引为i的父结点的索引是floor((i-1)/2);
在这里插入图片描述
堆,其实可以看成1个树结构,例如,对于最大堆{110,100,90,40,80,20,60,10,30,50,70}而言:索引为0的左孩子的所有是1;索引为0的右孩子是2;索引为8的父节点3。
整个堆建立大根堆,肯定是从局部的子堆都建立为大根堆开始,最后整个堆肯定为大根堆。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// 构建大根堆
void mySort::Build_max_headp(vector<int>& v, int start, int end)
{
	// 从当前节点构建局部大根堆
	int cur = start;
	// 左孩子
	int l = 2 * cur + 1;
	// 局部并不是比1次,万一左孩子或者右孩子还作为下一个局部子堆的根节点,因此要循环
	// 循环的下一次l就要更新为cur,左孩子更新2 * cur + 1
	// l可以=end,也就是没有右孩子
	for (; l <= end; cur=l, l = 2 * cur + 1)
	{ 
		// 如果左孩子小于右孩子,就拿右孩子和父节点比;就拿左孩子和父节点比
		// 但是这里的代码是比较左右孩子,如果没有右孩子,就不用比了,所以是小于
		if (l < end && v[l] < v[l + 1]) l++;
		if (v[l] > v[cur]) swap(v[l], v[cur]);
		else break;
	}
}

// 堆排序
// 时间复杂度:O(n*logn)
// 空间复杂度(额外):O(1) 
// 稳定性:不稳定
void mySort::HeadpSort(vector<int>& v)
{
	// 最开始的最后一个结点是:n / 2 - 1,然后--,直到最后0的根节点
	for (int i = v.size() / 2 - 1; i >= 0; i--)
	{
		Build_max_headp(v, i, v.size() - 1);
	}
	for (int  i = v.size() - 1; i > 0; i--)  // 时间复杂度:n
	{
		swap(v[0], v[i]);
		// 建立完1次大根堆,拿到最大数后,就不要再从下面就往上建立了,因为有些局部的已经建立好。
		// 我们这根节点0开始,每次减去最大数
		Build_max_headp(v, 0, i - 1);  // 时间复杂度:logn
	}
}

总结
1.凡是涉及到:二分倍增,分解也好,合并也好,一定少不了logn,
在这里插入图片描述
假设1个n个结点的满二叉树,树高为h,总结点数为:
2^0+2^1+2^2+...2^h-1 = n–>2^h - 1 = n–>h = log2(n+1) --> h = log2n(1可以忽略不计) ;建堆的过程就是从堆顶遍历到堆底的过程,所以时间复杂度就是logn。
在这里插入图片描述

3.8 递归

递归:在自定义函数种调用自己
递归的正确使用,必须加1个结束条件
递归特点
1.先进后出
2.代码简洁,理解 / 实现困难
递归求n项和的示例:

3.8.1 求n项和

int sum(int n)
{
	if (n == 1) return 1;  // 递归必须有出口--终止条件
	return n + sum(n - 1);  // 递归调用
}
// 第1层 sum(5)  return 5 + sum(4)
// 第2层 sum(4)  return 4 + sum(3)
// 第3层 sum(3)  return 3 + sum(2)
// 第4层 sum(2)  return 2 + sum(1)
// 递归必须有出口--终止条件
// 第5层 sum(1)  return 1
// 函数结束,没有下一层了
// 第5层 sum(1)  return 1 + sum(0)
// 第6层 sum(0)  return 0 + sum(-1)

3.8.2 递归打印1–n

// 递归打印1--n
void recursion_print(int n)
{
	if (n == 1)  // 递归必须有出口--终止条件
	{
		cout << n << " ";
		return;
	}
	recursion_print(n - 1);
	cout << n << " ";
}
// 递归是先进后出的特点,逐层结束时,就会执行cout << n << " ";
// 从下往上看,第5层一旦return结束,立刻回到第4层的recursion_print(1)函数调用,随后就会走cout<<2
// 第4层一旦结束,立刻回到第3层的recursion_print(2)函数调用,随后就会走cout<<3
// 第3层一旦结束,立刻回到第2层的recursion_print(3)函数调用,随后就会走cout<<2
// 第1层 recursion_print(5)  return recursion_print(4)  cout<<5
// 第2层 recursion_print(4)  return recursion_print(3)  cout<<4
// 第3层 recursion_print(3)  return recursion_print(2)  cout<<3
// 第4层 recursion_print(2)  return recursion_print(1)  cout<<2
// 第5层 recursion_print(1)  return;                    cout<<1

3.8.3 递归正序打印数组

// 递归正序打印数组
int a[] = { 20,40,30,10,60,50 };
void printArr(int index)
{
	if (index == 0)
	{
		cout << a[index] << " ";
		return;
	}
	printArr(index - 1);
	cout<< a[index] << " ";
}
// 第5层 printArr(5)  printArr(4)      cout<<a[5]    
// 第4层 printArr(4)  printArr(3)      cout<<a[4]    
// 第3层 printArr(3)  printArr(2)      cout<<a[3]    
// 第2层 printArr(2)  printArr(1)      cout<<a[2]    
// 第1层 printArr(1)  printArr(0)      cout<<a[1]                   
// 第0层 printArr(0)  cout<<a[0]    

3.8.4 递归逆序打印数组

// 递归逆序打印数组
int a[] = { 20,40,30,10,60,50 };

void printArr2(int index)
{
	if (index == 6) return;
	printArr2(index + 1);
	cout << a[index] << " ";
}
// 第6层 printArr(0)  printArr(1)      cout<<a[0]    
// 第5层 printArr(1)  printArr(2)      cout<<a[1]    
// 第4层 printArr(2)  printArr(3)      cout<<a[2]    
// 第3层 printArr(3)  printArr(4)      cout<<a[3]    
// 第2层 printArr(4)  printArr(5)      cout<<a[4]                   
// 第1层 printArr(5)  printArr(6)      cout<<a[5]  
// 第0层 printArr(6)  return;

3.8.5 二分搜索的递归版本

// 二分搜索(数据默认有序),以升序为例
// 时间复杂度:O(logn)数学归纳法,求数高
// 空间复杂度(额外):O(1)
// 稳定性:
int binarySearch(vector<int>& v, int l, int r, int target)
{
	// 有序序列:{1, 2, 4, 6, 8, 10, 12},二分查找11
	// 最左边数值1的索引为`l`,最右边数值12的索引为`r`,中间数值6的索引为`m`,
	// 11 > a[m]=6,所以更新:`l`更新到m+1的位置(数值8),`r`不变,中间数值10的索引为`m`,
	// 11 > a[m]=10,所以更新:`l`更新到m+1的位置(数值12),最后l、m+1、r(不变)都指向同一个数值12(其实当l和r重合时,就找不到了。)
	// 11 < a[m]=12,所以更新:l不变(指向数值12),`r`更新到m-1的位置(数值10),中间数值10的索引为`m`,
	

	// 终止条件,l = r重合时,还能再查一次
	if (l > r) return -1;
	int mid = (l + r) / 2;
	if (v[mid] == target)
	{
		return mid;
	}
	else if (v[mid] < target)
	{
		// 往右边查,l更新到m+1
		return binarySearch(v, mid + 1, r, target);
	}
	else if (v[mid] > target)
	{
		// 往左边查,r更新到m-1
		return binarySearch(v, l, mid - 1, target);
	}

	return -1; //都没查到

}

总结:
1.如果直接遍历数组查找,时间复杂度是:O(n);
2.但是,二分查找,每次都会舍弃一半,查找速度更快。

3.8.4 总结

1.先写终止条件。
2.肯定在函数体中调用函数本身,并且要逐个++ / --,直到走终止条件。
3.可以画逐层草稿进行推算。
4.本身就是一种递归。递归,其实也可以理解成一种单一的树结点,只是没有二分。
5.用到递归,很有可能涉及到左区l右区r中间mid
在这里插入图片描述

3.9 快速排序

在这里插入图片描述
在这里插入图片描述
先进后出,除了可以用递归,还可以用,但是栈相对复杂。

// 快速排序
void mySort::QuickSort(vector<int>& v, int l, int r)
{
	// 左右重合就不再查了
	if (l >= r) return;

	// 默认排序序列中最左侧元素是基准
	int temp = v[l];
	int i = l, j = r;
	while (i < j)
	{
		// 先看j,当v[j]一直大于temp的时候,j一直--
		while (i < j && v[j] > temp)
		{
			j--;
		}
		// 等到v[j] 小于 temp时,上述while结束,v[j]要往前放,这里将v[i] = v[j],然后i++
		if (i < j)
		{
			v[i++] = v[j];
		}

		// 再看i,当v[i]一直小于temp的时候,i一直++
		while (i < j && v[i] < temp)
		{
			i++;
		}
		// 等到v[i] 大于 temp时,上述while结束,v[i]要往后放,这里将v[j] = v[i],然后j--
		if (i < j)
		{
			v[j--] = v[i];
		}
	}

	// 当i和j相遇时,i/j的位置就是基准位,也就是临时存放的temp
	v[i] = temp;

	// 以基准分左右,开始分别进行快排
	QuickSort(v, l, i - 1);  // 左边
	QuickSort(v, i + 1, r);  // 右边
}

总结
1.快速排序,和二分查找类似,也是有左l中间值mid右r,并结合递归

3.10 归并排序

归并分解和二分搜索、快速排序类似,都是找lrmid。但是二分搜索是左右递归只走一个;快排是找基准,分左右时,两边快排。归并两边都要分解,直到分解到l=r重合时结束。递归,是先从上往下分解,再从下往上逐层返回,在返回的时候合并并排序,保证每个子区间是有序的。
在这里插入图片描述
在这里插入图片描述

// 归并排序
void mySort::MergeSort(vector<int>& v, int l, int mid, int r)
{
	int* tmp = new int[r - l + 1];
	// i从最左边开始,到mid结束
	int i = l;
	// j从mid+1开始,到r结束
	int j = mid + 1;
	int index = 0;
	while (i <= mid && j <= r)
	{
		// 取i和j中最小的
		if (v[i] < v[j])
		{
			tmp[index++] = v[i++];
		}
		else
		{
			tmp[index++] = v[j++];
		}
	}
	while (i <= mid) tmp[index++] = v[i++];
	while (j <= r) tmp[index++] = v[j++];

	for (int i = 0; i < index; i++)
	{
		v[l + i] = tmp[i];
	}
	delete[]tmp;
}

// 递归分解+合并+排序
// 递归的时间复杂度就是求树高,不是简单的算循环次数
// 时间复杂度:O(n*logn)  树高logn,每一层都要计算n次
// 空间复杂度(额外):O(n*logn) 
// 稳定性:稳定
void mySort::Merge_up2down(vector<int>& v, int l, int r)
{
	if (l >= r) return;
	int mid = (l + r) / 2;
	// 快排的mid不参与下次递归,而这里归并要参与
	Merge_up2down(v, l, mid);
	Merge_up2down(v, mid + 1, r);
	// 左递归、右递归都结束了,要针对这两个子数进行排序了。
	MergeSort(v, l, mid, r);
}

注意事项
1.递归的时间复杂度就是求树高,不是简单的算循环次数

4、贪心算法

4.1 引入

在这里插入图片描述
降序,让钱的面值最大的优先级最高,优先消耗

int total, n, ans;
int money[101];

bool cmp(int a, int b)
{
	return a > b;
}
int main()
{	
	// total钱的总数,n多少种纸币面值
	cin >> total >> n;
	for (int i = 0; i < n; i++)
	{
		cin >> money[i];
	}
	sort(money, money + n, cmp); // 降序,让钱的面值最大的优先级最高,优先消耗
	for (int i = 0; i < n; i++)
	{
		if (money[i] <= total)
		{
			int num = total / money[i];
			total -= num * money[i];
			ans += num;
		}
	}
	cout << ans << endl;
 	system("pause");
	return 0;
}

4.2 奥利凡德

在这里插入图片描述
要想全部放下,必须最小的魔杖对应最小的盒子;最大的魔杖对应最大的盒子。
在这里插入图片描述思路依旧是排序,选优先级

int n;
int a[101];
int b[101];


int main()
{	
	cin >> n;
	for (int i = 0; i < n; i++)
	{
		cin >> a[i];
	}
	for (int i = 0; i < n; i++)
	{
		cin >> b[i];
	}
	sort(a, a + n); 
	sort(b, b + n); 
	for (int i = 0; i < n; i++)
	{
		if (a[i] > b[i])
		{
			cout << "No" << endl;
			return 0;
		}
	}
	cout << "Yes" << endl;
 	system("pause");
	return 0;
}

4.3 采购商品

在这里插入图片描述

贪心算法都是冲个字去的,只有最才能产生优先思维

struct goods
{
	int price;
	int num;
};


int m, n;

bool cmp(goods a, goods b)
{
	return  a.price < b.price;
}
int main()
{	
	// m总价钱,n商品数量
	cin >> m >> n;
	struct goods a[101];
	for (int i = 0; i < n; i++)
	{
		cin >> a[i].price >> a[i].num;
	}
	sort(a, a + n , cmp); 
	int ans = 0;
	// 商品从最便宜的开始遍历
	for (int i = 0; i < n; i++)
	{
		// 对于当前商品尽可能多买,如果能买,全买
		if (m >= a[i].num * a[i].price)
		{
			ans += a[i].num;
			m -= a[i].num * a[i].price;
		}
		else
		{
			// 不能全买,能买多少是多少
			int num = m / a[i].price;
			ans += num;
			m -= num * a[i].price;

		}
	}
	cout << ans << endl;
 	system("pause");
	return 0;
}

4、递推算法

递推也是经常被使用的一种简单算法。递推是一种用若干步可重复的简单运算来描述复杂问题的方法。
递推的特点在于,每一项都和他前面的若干项有一定关联,这种关联一般可以通过递推关系式来表示,可以通过其前面若干项得出某项的数据。
对于递推问题的求解一般从初始一个若干个数据项出发,通过递推关系式逐步推进,从而得出想要的结果,这种求解问题的方法叫递推法。其中,初始的若干数据项称为边界

4.1 一级递推

在这里插入图片描述

4.2 递推求兔子问题

递归与递推的区别
1、递归是从前往后,找完以后再推回来。
2、递推是一波直接推完,把每个阶段的值记录下来,后面的值只跟前面的值有关系。用空间换时间。
在这里插入图片描述

int f[101];
// 递推算法
// 1.边界 f[1]=f[2]=1
// 2.递推关系式 f[i]=f[i-1] + f[i-2]

// 时间复杂度:O(n)
int main()
{
	int n;
	cin >> n;
	f[1] = f[2] = 1;
	for (int i = 3; i <= n; i++)
	{
		f[i] = f[i - 1] + f[i - 2];
	}
	cout << f[n];
	cout << endl;
	system("pause");
	return 0;
}

x.1 二分搜索

要从1-1000的数里面查找某个值,最快应该是折半查找,如果这个数比500大,那1-500的范围就不用再找了。
二分搜索的前提是有序序列:{1, 2, 4, 6, 8, 10, 12},设最左边数值1的索引为l,最右边数值12的索引为r,中间数值6的索引为m,比如要查找10这个数,10 > a[m],所以直接从m + 1往后查找就行,继续取m + 1r区间的中间索引mm,继续比较a[mm] 和 要找的10,重复这个过程。
二分搜索有两种情况:1、用循环写;2、用递归写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Andrew_Xzw

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值