到了期待的数据结构板块了,数据结构、算法、时间复杂度、空间复杂度概念,如何计算时间复杂度和空间复杂度呢?一起来看看吧。
数据结构
是计算机存储、组织数据的方式,是相互之间存在一种或多种特定关系的数据元素的集合。
算法
是定义良好的计算过程,取一个或一组的值为输入,并产生出一个或一组值作为输出。可以认为是一系列的计算步骤,用来将输入数据转化成输出结果。
时间复杂度
算法效率分为两种:时间效率(时间复杂度)和空间效率(空间复杂度) ,时间复杂度衡量算法运行速度,空间复杂度衡量算法需要的额外空间。现在计算机存储容量大,已经不再需要关注空间复杂度。时间复杂度算的不是时间,是操作执行的次数。
大O的渐进表示法
1.用常数1取代运行时间中的所有加法常数
2.在修改后的运行次数函数中,只保留最高阶项
3.如果最高阶项存在且不为1,则去除这个项目相乘的常数,得到的结果就是大O阶。
时间复杂度的计算
举例1:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void Func3(int N, int M)
{
int count = 0;
int k = 0;
for (k = 0; k < M; k++)
{
++count;
}
for (k = 0; k < N; k++)
{
count++;
}
printf("%d\n", count);
}
时间复杂度为O(M+N),不能确定谁对结果的影响大,所以M和N都要留下来。如果已经明确N>>M,那么时间复杂度为O(N)。
举例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; k++)
{
++count;
}
printf("%d\n", count);
}
for循环了100次,即常数次,时间复杂度为O(1)。
举例3:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
const char* strchr(const char* str, int character)
{
while (*str != '\0')
{
if (*str == character)
{
return str;
}
str++;
}
return str;
}
假设字符串的长度是N,那么最好的情况是字符串第一个字符就是要查找的字符,时间复杂度为O(1),最坏的情况是字符串最后一个字符是要查找的字符,时间复杂度为O(N)。虽然平均情况是查找N/2次,但是默认时间复杂度是看最坏的,即O(N)。
举例4:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
void Swap(int *p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void BubbleSort(int* a,int n)
{
assert(a);
int end = 0;
for (end = n; end > 0; --end)
{
int exchange = 0;
for (int i = 1; i < end; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
冒泡算法按照最坏的情况分析,即假如数组按照从大到小的顺序排列,那么第一个元素找到最终位置需要比较N-1次,第二个元素找到最终位置需要N-2次,·······,最后一个元素找到最终位置需要1次,平均计算次数为(N-1)+(N-2)+······+1=N*(N-1)/2。按照大O渐进表示法第2条,时间复杂度为O(N^2)。
递归算法时间复杂度的计算
递归算法时间复杂度=递归次数*每次递归函数中次数
举例1:
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N - 1) * N;
}
计算递归函数调用的次数:
每次调用Factorial函数,只执行return这一句,因此每次递归函数中次数=1,还需计算递归次数:
第一次递归,需计算F(N)
第二次递归,需计算F(N-1)
……
最后一次递归,需计算F(1)
一共计算了N次,因此时间复杂度=N*1=O(N)。如果将递归函数体的代码改成for循环:
for(i = 0; i<N; i++)
{
}
时间复杂度=O(N*N)=O(N^2)。
举例2:
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N - 1) * Factorial(N - 2);
}
递归次数:
右边的计算会提前结束,有一部分会缺失,因此在满的情况下,计算次数=2^0+2^1+2^2+……+2^(N-2)+2^(N-1)=2^N-1。那么缺的情况下计算次数=2^N-1-常数(右边缺的),时间复杂度为O(2^N)。
时间复杂度应用
1.力扣网-消失的数字
数组nums
包含从0
到n
的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?
分析:3种思路:
(1)冒泡排序或快排对数组先排序,找出第一个元素值不等于该元素下标的元素。(O(N^2)或O(NlogN))
思路(1)代码:
//冒泡排序
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
void BubbleSort(int* nums, int numSize)
{
int i = 0;
for (i = 0; i < numSize-1; i++)
{
int j = 0;
for (j = 0; j < numSize-i-1; j++)
{
if (nums[j] > nums[j+1])
{
int temp = nums[j];
nums[j] = nums[j+1];
nums[j + 1] = temp;
}
}
}
}
int missingNumber(int* nums, int numSize)
{
BubbleSort(nums, numSize);
int i = 0;
for (i = 0; i < numSize; i++)
{
if (nums[i] != i)
{
return i;
}
}
return -1;
}
int main()
{
int arr[5] = { 5,2,4,1,0 };
int ret = missingNumber(arr, 5);
printf("%d\n", ret);//3
return 0;
}
//快排
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int cmp(const void* e1, const void* e2)
{
return *(int*)e1 - *(int*)e2;
}
int missingNumber(int* nums, int numSize)
{
qsort(nums, numSize, sizeof(nums[0]), cmp);
int i = 0;
for (i = 0; i < numSize; i++)
{
if (nums[i] != i)
{
return i;
}
}
return -1;
}
int main()
{
int arr[5] = { 5,2,4,1,0 };
int ret = missingNumber(arr, 5);
printf("%d\n", ret);//3
return 0;
}
(2)计算0~n的和S1,再计算数组所有元素的和S2,S1-S2。(O(N))
思路(2)代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int missingNumber(int* nums, int numSize)
{
int i = 0;
int sumOfNums = 0;
int sumOfIndex = 0;
for (i = 0; i < numSize; i++)
{
sumOfNums += nums[i];
}
for (i = 0; i < numSize + 1; i++)
{
sumOfIndex += i;
}
return sumOfIndex - sumOfNums;
}
int main()
{
int arr[5] = { 5,2,4,1,0 };
int ret = missingNumber(arr, 5);
printf("%d\n", ret);//3
return 0;
}
(3)用0和数组中每一个元素顺序异或:0^a1^a2^……^an,之后再和每个下标顺序异或,结果就是缺失的数字。(O(N))
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int missingNumber(int *nums,int numSize)
{
int i = 0;
int x = 0;
for (i = 0; i < numSize; i++)
{
x ^= nums[i];
}
for (i = 0; i < numSize + 1; i++)
{
x ^= i;
}
return x;
}
int main()
{
int arr[5] = { 5,2,4,1,0 };
int ret = missingNumber(arr, 5);
printf("%d\n", ret);//3
return 0;
}
2.力扣网-只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。说明:你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
分析:
线性时间复杂度即时间复杂度为O(N),不使用额外空间即空间复杂度为O(1)。根据题目,如果用0异或数组中的所有元素,那么出现两次的元素就被异或为0,剩下的那个元素就是只出现一次的数字。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int singleNumber(int* nums, int numsSize) {
int x = 0;
for (int i = 0; i < numsSize; i++)
{
x ^= nums[i];
}
return x;
}
int main()
{
int arr[5] = { 3,1,1,5,3 };
int len = sizeof(arr) / sizeof(arr[0]);
int ret = singleNumber(arr, len);
printf("%d", ret);//5
return 0;
}
3.力扣网-数组中数字出现的次数
一个整型数组 nums
里除两个数字之外,其他数字都出现了两次。请写程序找出这两个只出现一次的数字。要求时间复杂度是O(n),空间复杂度是O(1)。
分析:
(1)如何将原数组分成两组--将0和数组所有元素异或的结果,将该结果从倒数第一位开始查找第j位为1为a组,第j位为0的为b组,如1(001),1(001),5(101)的倒数第二位都为0,如2(010),2(010),3(011)的倒数第二位都为1,基于此可将数组分为两组。
(2)结果x和y一定会分别进入a、b组,其他出现两次的数,要么进第一组,要么进第二组
(3)第一组和第二组数据就变成其他数出现2次,只有一个数出现1次
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int* singleNumbers(int* nums, int numsSize, int* returnSize)
{
//将所有元素进行异或
int ret = 0;
int i = 0;
for(i = 0;i<numsSize;i++)
{
ret ^= nums[i];
}
//找出第一个为1的j位
int j = 0;
for(j = 0;j<32;j++)
{
if((ret >> j) & 1)
{
break;
}
}
//按照第j位为0或1将数组分在两组,并对两组元素分别异或
int x = 0,y = 0,k = 0;
for(k = 0;k<numsSize;k++)
{
if((nums[k] >> j) & 1)
{
x ^= nums[k];
}
else
{
y ^= nums[k];
}
}
int *arr = (int *)malloc(sizeof(int)*2);
*returnSize = 2;
arr[0] = x;
arr[1] = y;
return arr;
}
int main()
{
int array[6] = { 1,3,5,2,1,2 };
int* p = array;
p = singleNumbers(array, 6, p);
for (int i = 0; i < 2; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
4-力扣网-数组中数字出现的次数
在一个数组 nums
中除一个数字只出现一次之外,其他数字都出现了三次。请找出那个只出现一次的数字。
分析:需要先统计32位中,第1位有多少1,第2位有多少1,第31位有多少1,让每一位的结果%3,找出余数为1的那些位,这些位的组合就是出现一次的数
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<math.h>
int singleNumber(int* nums, int numsSize) {
int i = 0, j = 0, bit = 0, ret = 0;
for(j = 0; j < 32; j++)
{
bit = 0;
for(i = 0; i < numsSize; i++)
{
bit += (nums[i]>>j)&1;
}
ret += ((bit % 3) <<j);
}
return ret;
}
int main()
{
int arr[4] = { 3,6,6,6 };
int len = sizeof(arr)/sizeof(arr[0]);
int ret = singleNumber(arr,len);
printf("%d", ret);
return 0;
}
时间复杂度结束了,空间复杂度就要开始了~
空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度不是计算程序占用了多少字节的空间,算的是变量的个数,也使用大O渐进表示法。
举例1:
//计算冒泡排序的空间复杂度
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
void Swap(int *p1, int *p2)
{
int temp = *p1;
*p1 = *p2;
*p2 = temp;
}
void BubbleSort(int* a,int n)
{
assert(a);
int end = 0;
for (end = n; end > 0; --end)
{
int exchange = 0;
for (int i = 1; i < end; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
计算空间复杂度时,不考虑输入数组的空间,计算的是算法运行中额外使用的空间。对于时间来说,是累计的,但是空间是不累计的,可以复用的(如定义了一个end变量,当执行for循环end不断--时,依旧使用end变量的空间)。该算法定义了4个变量:end、exchage、i、temp,因此额外使用了4个空间,即额外使用了常数个空间,根据大O渐进表示法,空间复杂度为O(1)。
举例2:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
long long* Fibonacci(size_t n)
{
if (n == 0)
{
return NULL;
}
long long* fibArray = (long long*)malloc(sizeof(long long) * (n + 1));
fibArray[0] = 0;
fibArray[1] = 1;
for (int i = 2; i <= n; i++)
{
fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
}
return fibArray;
}
动态开辟了n+1个空间,空间复杂度为O(N)。
递归算法空间复杂度的计算
递归算法的空间复杂度等于递归的深度(递归的层数),递归多少层就建立多少个栈帧,每建立一个栈帧空间复杂度就是O(1),即建立常数个空间。
long long Factorial(size_t N)
{
return N < 2 ? N : Factorial(N - 1) * N;
}
递归的层数为N,空间复杂度为O(N)。
空间复杂度应用
1.力扣网-移除元素
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
分析:空间复杂度为O(1),即不允许创建额外数组,在原数组上进行操作。可以使用快慢指针的原理,当快指针当前指向的元素值如果不等于val,对快指针+1,如果等于val,将快指针的值赋给慢指针,将快指针和慢指针都+1。
代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int removeElement(int* nums, int numsSize, int val)
{
if (numsSize == 0)
{
return 0;
}
int src = 0;
int dst = 0;
int i = 0;
for (i = 0; i < numsSize; i++)
{
if (nums[i] != val)
{
nums[dst] = nums[src];
src++;
dst++;
}
else
{
src++;
}
}
return dst;
}
int main()
{
int arr[] = { 6,3,2,5,2,1,3,2 };
int len = sizeof(arr) / sizeof(arr[0]);
int i = 0;
int ret = removeElement(arr, len, 2);
for (i = 0; i < ret; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
执行结果:
2.力扣网-删除有序数组中的重复项
给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。
要求不要使用额外的数组空间,你必须在原地修改输入数组 并在使用 O(1) 额外空间的条件下完成。
分析:要求不要使用额外的数组空间,就必须在原数组上进行操作。使用3个指针,cur指向数组不同元素的第一个,next寻找下一个不同的元素,dst指向新的存放的不同元素的位置。当next和cur指向的元素值不同时,就把next指向的元素存放到dst的位置,cur、next、dst都++,当next和cur指向的元素值相同时,next++直到next指向的元素和cur指向的元素不相等,将dst++,cur=next,next++
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
int removeDuplicates(int* nums, int numsSize)
{
if (numsSize == 0)
{
return 0;
}
int cur = 0;
int next = 1;
int dst = 0;
while (next < numsSize)
{
if (nums[next] != nums[cur])
{
nums[dst++] = nums[cur++];
next++;
}
else
{
//nums[next] = nums[cur],不用做操作,next++,直到nums[next] != nums[cur]
while ((next < numsSize) && (nums[next] == nums[cur]))
{
next++;
}
//现在nums[next] != nums[cur],nums[dst] = nums[cur]
nums[dst] = nums[cur];
//dst向后移动一个位置
dst++;
//cur直接移动到next的位置
cur = next;
//next向后移动一个位置
next++;
}
}
//当next走完整个数组后,直接将cur位置的值赋值给dst++位置
if (cur < numsSize)
{
nums[dst++] = nums[cur];
}
return dst;
}
int main()
{
int arr[] = { 1,1,2,3,4,4,9,9,9,10 };
int len = sizeof(arr) / sizeof(arr[0]);
int ret = removeDuplicates(arr, len);
int i = 0;
for (i = 0; i < ret; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
执行结果: