目录
前言
算法与复杂度
时间复杂度
大O的渐进表示法
时间复杂度计算练习
空间复杂度
题目练习
题目一
题目二
前言
1. 什么是数据结构?
数据结构(Data Structure)是计算机存储、组织数据的方式,指相互之间存在一种或多种特定关系的数据元素的集合。
我们在前面写的通讯录,其实就是一个数据结构
2.什么是算法?
算法(Algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
这里的概念性的内容看一下就行,后面才是重点
算法与复杂度
衡量一个算法好坏的标准是什么?我相信有一些和我一样的小白,曾天真的认为只要代码写的简洁,这就是一个好代码,就比如之前学过的斐波那契数,很简单的一个递归就可以解决。如下:
long long Fib(int N) { if(N < 3) return 1; return Fib(N-1) + Fib(N-2); }
简简单单三行代码,就可以实现,但是我们发现,假如我们输入的数字大一点,我们的计算机就计算不出来了,比如输入一个50,就会出现如下情况:
但假如我们换另一种方式,便可以很快的打印出来:
所以,我们不能仅仅简单的以代码的长短来衡量代码的好坏,而衡量一个算法,主要是看它的复杂度。
算法与复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。时间复杂度才是如今的主要关注点
接下来我们便来瞅一瞅时间复杂度以及空间复杂度
时间复杂度
概念
这里官方的定义比较繁多,我们只需要知道,时间复杂度就是算法中的基本操作的执行次数,就比如下面这段代码:
#include<stdio.h> int main() { int n = 0; int count = 0; scanf("%d", &n); for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { count++; } } for (int i = 0; i < 2 * n; i++) { count++; } for (int i = 0; i < 10; i++) { count++; } printf("%d", count); return 0; }
这里count一共执行了n^2+2*n+10次,这就是这个算法的时间复杂度。但实际上,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,这里就提到了大O的渐进表示法。
大O的渐进表示法
推导大O阶方法:
1、用常数1取代运行时间中的所有加法常数。
2、在修改后的运行次数函数中,只保留最高阶项。
3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶
这里我们根据这个推导公式,就可以判断,我们上面的时间复杂度用大O的渐进表示就是O(N^2)
但是对于有些算法,它可能存在最优、平均以及最坏可能,就比如我们在N个数据中,查找我们想要的数字val,它可能存在以下三中可能:
最优:一次就找到
平均:找了N/2次
最坏:最后才找到,即找了N次
那么这里我们取的是最坏情况,所以这里的时间复杂度为O(N)。
为什么会不取最优或者平均呢?“凡事做好最坏的打算”,这里的凡事就包含了这个。
时间复杂度计算练习
题目一:
// 计算Func2的时间复杂度? void Func2(int N) { int count = 0; for (int k = 0; k < 2 * N ; ++ k) { ++count; } int M = 10; while (M–) { ++count; } printf(“%d\n”, count); }
一共运行2*N+10次
时间复杂度:O(N),因为这里的最高阶项为1,然后去掉最高阶项的常数,就是N
题目二:
// 计算Func3的时间复杂度? void Func3(int N, int M) { int count = 0; for (int k = 0; k < M; ++ k) { ++count; } for (int k = 0; k < N ; ++ k) { ++count; } printf(“%d\n”, count); }
O(M+N),因为有两个未知数M、N
题目三:
// 计算Func4的时间复杂度? void Func4(int N) { int count = 0; for (int k = 0; k < 100000000; ++ k) { ++count; } printf(“%d\n”, count); } O(1),因为100000000是个常数,常数都用O(1)表示 题目四:冒泡排序的时间复杂度 void bubble_sort(int sz, int *arr) { int t = 0;//趟数 for (t = 0; t < sz - 1; t++) { int j = 0; for (j = 0; j < sz - 1 - t; j++) { if (arr[j] > arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j+1]=tmp; } } } }
冒泡排序,第一次排序N个,第二次N-1,第三次N-2,第四次N-3…第N次1
加起来刚好是个等差数列求和即(N*(N+1))/2,这里展开后,取最高项,并且去掉常数,就算O(N^2)
题目四:二分查找时间复杂度
int BinarySearch(int* a, int n, int x) { assert(a); int begin = 0; int end = n-1; // [begin, end]:begin和end是左闭右闭区间,因此有=号 while (begin <= end) { int mid = begin + ((end-begin)>>1); if (a[mid] < x) begin = mid+1; else if (a[mid] > x) end = mid-1; else return mid; } return -1; }
所以时间复杂度为O(logN) 、有时也会这么写->O(lgN)
空间复杂度
简单来说空间复杂度算的是变量的个数。也使用大O渐进表示法,规则与时间复杂度相同。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
例题:
void BubbleSort(int* a, int n) { assert(a); for (size_t end = n; end > 0; --end) { int exchange = 0; for (size_t i = 1; i < end; ++i) { if (a[i-1] > a[i]) { Swap(&a[i-1], &a[i]); exchange = 1; } } if (exchange == 0) break; } }
这里的数组a以及n,都是函数参数,是运行这个函数时已经创建好了的,只有变量i、end、还有exchange是我们在运行这个函数的时候才申请空间,所以是常数个,所以空间复杂度O(1)
题目练习
题目一
数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
例:输入:[9,6,4,2,3,5,7,0,1]
输出:8
分析:这里我们有三种思路,第一种时是进行求和,求0-n的和(等差数列公式:(n+1)n/2直接算出来),然后遍历数组,进行求和,两者再相减即可。
时间复杂度O(N)√
第二种:
先排序,再遍历,我们知道,冒泡排序的时间复杂度为O(N^2),而qsort时间复杂度是O(NlogN),两者都不符合,所以直接不考虑这种
第三种:
异或法,两两相等的数异或为0,将数组元素异或,再将异或结果与0-n异或,就可找出缺失的数。
时间复杂度O(N)√
思路一实现:
int missingNumber(int* nums, int numsSize) { int n = numsSize; //0-n之和 int sum = n * (n + 1) / 2; int i = 0; int m = 0; //数组元素之和 for (i = 0; i < n; i++) { m += nums[i]; } //返回两者之差 return sum - m; }
思路二实现:
int missingNumber(int* nums, int numsSize) { int ret = 0; int i = 0; int j = 0; //先异或所有元素 for (i = 0; i < numsSize; i++) { ret ^= nums[i]; } //再把所有元素异或从0-numsSize,找到单身狗 for (j = 0; j < numsSize + 1; j++) { ret ^= j; } return ret; }
题目二
给你一个数组,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右轮转 1 步: [7,1,2,3,4,5,6]
向右轮转 2 步: [6,7,1,2,3,4,5]
向右轮转 3 步: [5,6,7,1,2,3,4]
思路一:空间换时间,开辟一个新数组,先拷贝后面的k个元素,再拷贝前面的元素,再把整个数组的元素拷贝到原数组。
时间复杂度O(N)、空间复杂度O(N)
实现:
void rotate(int* nums, int numsSize, int k){ k%= numsSize; int arr[numsSize]; //先拷贝后面k个元素 int j=0; for(int i=numsSize-k; i<numsSize; i++) { arr[j]=nums[i]; ++j; } //再拷贝前面的元素 for(int i=0; i<numsSize-k; ++i) { arr[j]=nums[i]; ++j; } //整体拷贝 for(int i=0; i<numsSize; ++i) { nums[i]=arr[i]; } }
思路二:三次反转法、 时间复杂度:O(n),空间O(1)
先反转k前面的数组元素,再反转后面k个元素,最后再整体反转
例:1 2 3 4 5 k=2
第一次:3 2 1 4 5
第二次:3 2 1 5 4
第三次:4 5 1 2 3
实现:
void rotate(int* nums, int numsSize, int k) { //三次反转 k %= numsSize; int* right1 = nums + numsSize - 1; int* left1 = right1 - k + 1; int* right = left1 - 1; int* left = nums; int tmp = 0; while (left1 < right1) { tmp = *right1; *right1 = *left1; *left1 = tmp; left1++; right1--; } while (left < right) { tmp = *left; *left = *right; *right = tmp; left++; right--; } left = nums; right1 = nums + numsSize - 1; while (left < right1) { tmp = *left; *left = *right1; *right1 = tmp; left++; right1--; } }
思路三:一个一个旋转,旋转k次 时间复杂度O(N^2),空间O(1)
void rotate(int* nums, int numsSize, int k) { int* right = nums + numsSize - 1; int i = 0; k %= numsSize; while (k--) { int tmp = *right; for (i = 1; i < numsSize; i++) { *(nums + numsSize - i) = *(nums + numsSize - 1 - i); } *nums = tmp; } }
(思路三最后未通过测试)