目录
一.时间复杂度
1.概述
一个操作如果和样本的数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度为一个算法流程中,常数操作数量的一个指标。常用0(读作big 0)来表示。具体来说,先要对一个算法流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为0(f(N))。
时间复杂度表示某个算法运行时间的趋势,可以大致评判程序的效率。 评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是“常数项时间”。
2.时间复杂度表达
列出需要执行程序的次数,写成T(N)=aN²+bN+C的形式。此时,以存在的最高次项作为时间复杂度,因为N趋于无穷时,只有最高次项对结果的影响大。
例如:列出式子为aN³+bN+C,时间复杂度即为O(N³)。
具体的高阶低阶排行如下图:
如果阶数指标相同,无法通过常数项比较时间复杂度,只能通过实际跑代码区分。
3.简便计算时间复杂度
一般来说,最内层执行次数最多的语句就决定了整个算法的趋势,通常看最内层语句执行次数的规律即可。
4.注意事项
对于任何算法流程,估计时间复杂度O时,按照最差情况估计。
二.简单算法
1.异或运算
(1)交换操作
此时a和b必须拥有不相同的内存空间,因为同样的内存区域跟自己异或,会直接变成0。
void swap(int,a, int b)
{
a = a ^ b;
b = a ^ b;
a = a ^ b;
}
(2)异或算法应用1
问题:一个数组中,只有1个数出现奇数次,其他数均出现偶数次。求出现奇数次的这个数。
解法:定义一个eor = 0,将数组中所有数与其异或,异或出的eor即为解。因为异或运算同一批数,不管顺序如何,异或结果一样。所以等同于偶数次的数先异或。
原理:异或的交换律。
#include<iostream>
#include<string>
#include<math.h>
using namespace std;
void solve(int num[]);
int main() {
//算法问题1:问题:一个数组中,只有1个数出现奇数次,其他数均出现偶数次。求出现奇数次的这个数
//解法:定义一个eor = 0,将数组中所有数与其异或,异或出的eor即为解。
int nums[] = { 1,2,1,2,1,3,2,1,2,3,3 };
solve(nums);
system("pause");
return 0;
}
void solve(int num[]) {
int eor = 0;
int len = sizeof(num) / sizeof(num[0]);
for (int i = 0; i < len; i++) {
eor = eor ^ num[i];
}
cout << "奇数数字为:" << eor << endl;
}
(3)异或算法应用2
问题:一个数组中,只有2个数出现奇数次,其他数均出现偶数次。求出现奇数次的这2个数。
解法:首先通过对所有元素进行异或操作来找到两个只出现奇数次的数字的异或结果eor。然后,它找到eor中最右侧为1的位(这意味着这两个数字在这一位上是不同的),并根据这个位将所有的数字分成两组。每组内部再次进行异或操作,由于除了一个数字外,其他所有数字都会出现两次,这样就可以分别找到这两个只出现了奇数次的数字。
代码如下:(实现流程同算法应用1一样,这里只展示函数)
void solve2(int num[], int len) {
int eor = 0;
for (int i = 0; i < len; i++) {
eor ^= num[i];
}
int rightOne = eor & (~eor + 1); // 得到eor中最右侧的1
int onlyOne = 0; // 用于记录第一个只出现一次的数
for (int i = 0; i < len; i++) {
if ((num[i] & rightOne) != 0) {//分成两组
onlyOne ^= num[i];
}
}
int anotherOne = onlyOne ^ eor; // 得到第二个只出现一次的数
cout << "两个奇数次数字分别为:" << onlyOne << " 和 " << anotherOne << endl;
}
2.插入排序
简介:插入排序是一种简单的排序算法,通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。这个算法的时间复杂度一般是O(n²),适合于小数据量的排序。(ps:与其相似的冒泡排序在上一篇C++基础练习中)
步骤:
1.开始排序:算法从数组的第二个元素开始,即假定第一个元素已经被排序了。
2.选取元素:选取下一个元素,在已经排序的元素序列中从后向前扫描。
3.比较与插入:
(1)如果该元素(已排序的)大于新元素,将该元素移到下一位置。
(2)重复步骤3,直到找到已排序的元素小于或等于新元素的位置。
4.将新元素插入:将新元素插入到该位置后。
5.重复步骤:重复步骤2~4,直到整个数组排序完成。
#include<iostream>
#include<string>
#include<math.h>
using namespace std;
void insertpaixu(int num[],int len);
int main() {
int nums[] = {7,6,5,3,2,1,4 };
int len = sizeof(nums) / sizeof(nums[0]);
insertpaixu(nums, len);
system("pause");
return 0;
}
void insertpaixu(int num[], int len) {
for (int i = 1; i < len; i++) {
for (int j = i - 1; j >= 0; j--) {
if (num[j] > num[j + 1]) {
num[j] = num[j] ^ num[j + 1];
num[j + 1] = num[j] ^ num[j + 1];
num[j] = num[j] ^ num[j + 1];
}
else {
break;
}
}
}
cout << "排序后的数组顺序为:" << endl;
for (int i = 0; i < len; i++) {
cout << num[i] << " ";
}
}
3.二分法
原理:二分查找法(Binary Search)是一种在有序数组中查找特定元素的搜索算法。其工作原理是通过不断地将数组分成两半,比较中间元素与目标值,以确定目标值是在中间元素的左侧还是右侧,然后选择相应的一半继续搜索,直到找到目标值或搜索范围为空。
时间复杂度:二分查找的时间复杂度为O(log n)(没写底默认以2为底),其中n是数组中的元素数量。这是因为每次比较后,搜索的范围都会缩小一半,因此所需的步骤数量与数组大小的对数成正比。
注意:二分法不一定只用于有序数组。比如求解局部最小值问题等。
示例代码:(局部最小值)
#include <iostream>
#include <vector>
using namespace std;
int findLocalMin(const vector<int>& arr) {
int n = arr.size();
if (n == 0) return -1; // 空数组
if (n == 1 || arr[0] < arr[1]) return 0; // 数组首元素是局部最小
if (arr[n - 1] < arr[n - 2]) return n - 1; // 数组末元素是局部最小
int left = 1;
int right = n - 2;
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] < arr[mid - 1] && arr[mid] < arr[mid + 1]) {
return mid; // 找到局部最小值
}
else if (arr[mid] > arr[mid - 1]) {
right = mid - 1; // 局部最小值在左侧
}
else {
left = mid + 1; // 局部最小值在右侧
}
}
return -1; // 如果没有找到局部最小值
}
int main() {
vector<int> arr = { 9, 6, 3, 14, 5, 7, 4 };
int index = findLocalMin(arr);
if (index != -1) {
cout << "局部最小值位于索引 " << index << ", 值为 " << arr[index] << endl;
}
else {
cout << "没有找到局部最小值" << endl;
}
return 0;
}
4.对数器
概念:
1.有一个你想要测的方法a;
2.实现一个绝对正确但是复杂度不好的方法b;
3.实现一个随机样本产生器;
4.实现对比算法a和b的方法;
5.把方法a和方法b比对多次来验证方法a是否正确;
6.如果有一个样本使得比对出错,打印样本分析是哪个方法出错;
7.当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
作用:检查算法是否完全正确。
示例代码:
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstdlib> // for rand() and srand()
#include <ctime> // for time()
using namespace std;
// 生成随机数组
vector<int> generateRandomArray(int maxSize, int maxValue) {
vector<int> arr;
int size = rand() % (maxSize + 1); // 生成随机大小的数组
for (int i = 0; i < size; i++) {
arr.push_back(rand() % (maxValue + 1) - rand() % maxValue);
}
return arr;
}
// 比较器,使用标准库的sort函数
void comparator(vector<int>& arr) {
sort(arr.begin(), arr.end());
}
// 判断两个数组是否完全相等
bool isEqual(const vector<int>& arr1, const vector<int>& arr2) {
if (arr1.size() != arr2.size()) return false;
for (size_t i = 0; i < arr1.size(); i++) {
if (arr1[i] != arr2[i]) return false;
}
return true;
}
// 选择排序
void selectSort(vector<int>& arr) {
if (arr.size() < 2) return;
for (size_t i = 0; i < arr.size() - 1; i++) {
size_t minIndex = i;
for (size_t j = i + 1; j < arr.size(); j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
swap(arr[i], arr[minIndex]);
}
}
}
int main() {
srand(static_cast<unsigned int>(time(nullptr))); // 初始化随机数种子
int testTime = 500000;
int maxSize = 100;
int maxValue = 100;
bool succeed = true;
for (int i = 0; i < testTime; i++) {
vector<int> arr1 = generateRandomArray(maxSize, maxValue);
vector<int> arr2 = arr1; // 拷贝数组
selectSort(arr1);
comparator(arr2);
if (!isEqual(arr1, arr2)) {
succeed = false;
for (auto num : arr1) cout << num << " ";
cout << endl;
for (auto num : arr2) cout << num << " ";
cout << endl;
break;
}
}
cout << (succeed ? "方法可行!" : "方法有错误!") << endl;
return 0;
}
三.空间复杂度
1.概念
空间复杂度是对一个算法在运行过程中临时额外占用存储空间大小的量度 。空间复杂度不是程序占用 了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
2.空间复杂度计算和表示
函数运行时所需要的内存栈空间(储存参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数运行时显式申请的额外空间来确定。
注意:一个程序运行需要额外定义变量个数
(1)计算步骤:
1.固定空间需求:首先计算算法中所有非递归和非动态分配的存储需求,这通常包括固定大小的变量和数据结构等。
2.动态空间需求:接着计算算法中因为动态分配(如动态数组、链表等)而变化的存储需求。
3.递归空间需求:如果算法使用递归,那么还需要计算递归调用栈所需的空间。递归深度乘以每次递归所需的空间可以估算递归过程的空间需求。
4.总空间复杂度:将上述所有空间需求加总,得到算法的总空间复杂度。
(2)空间复杂度的表示
1.常数空间O(1):如果算法所需的临时空间不随输入数据的大小变化,即算法所需空间是固定的。那么该算法的空间复杂度为O(1)。例如,使用有限几个变量进行基本操作的算法。
2.线性空间O(n):如果算法所需的空间与输入数据的大小成正比,那么该算法的空间复杂度为O(n)。例如,需要复制整个输入列表的算法。
3.更复杂的空间复杂度:在更复杂的情况下,空间复杂度可能是O(n^2)、O(log n)等,这取决于算法对空间的需求如何随输入数据的规模增长。
3.示例代码
(1)空间复杂度O(1)
#include <iostream>
using namespace std;
void printSum(int arr[], int n) {
int sum = 0; // O(1) space
for (int i = 0; i < n; i++) {
sum += arr[i];
}
cout << "Sum is: " << sum << endl;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printSum(arr, n);
return 0;
}
在这个示例中,无论输入数组arr
的大小如何,printSum
函数总是只需要一个额外的变量sum
来计算数组的总和。因此,这个函数的空间复杂度是O(1)。
(2)空间复杂度O(n)
#include <iostream>
#include <vector>
using namespace std;
vector<int> duplicateArray(int arr[], int n) {
vector<int> dupArr; // O(n) space
for (int i = 0; i < n; i++) {
dupArr.push_back(arr[i]);
}
return dupArr;
}
int main() {
int arr[] = {1, 2, 3, 4, 5};
int n = sizeof(arr) / sizeof(arr[0]);
vector<int> dupArr = duplicateArray(arr, n);
for (int i : dupArr) {
cout << i << " ";
}
cout << endl;
return 0;
}
在这个示例中,duplicateArray
函数创建了一个与输入数组arr
大小相同的新向量dupArr
。因此,所需的额外空间与输入数组的大小成线性关系,空间复杂度是O(n)。