数据结构初阶--绪论

目录

一.什么是数据结构

二.什么是算法

三.算法的时间复杂度

四.算法的空间复杂度

五.复杂度练习

题一:消失的数字

题二:旋转数组


一.什么是数据结构

数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。

数据结构的三要素:

数据结构初阶--绪论_第1张图片

二.什么是算法

程序=数据结构+算法。数据结构研究如何把现实世界的问题信息化,将信息存进计算机。同时还要实现对数据结构的基本操作。算法研究如何处理这些信息,解决实际问题。

数据结构初阶--绪论_第2张图片

三.算法的时间复杂度

数据结构初阶--绪论_第3张图片

算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。一个算法所花费的时间与其中语句的执行次数成正比,算法中的基本操作的执行次数,为算法的时间复杂度。

实际中我们计算时间复杂度时,其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。

大O符号:是用于描述函数渐进行为的的数学符号。推导大O阶方法:

  1. 用常数1取代运行时间中的所有加法常数;
  2. 在修改后的运行次数函数中,只保留最高项;
  3. 如果最高阶项存在且系数不是1,则去除与这个项相乘的系数,得到的结果就是大O阶。

另外有些算法的时间复杂度存在最好,平均和最坏情况:

  1. 最坏情况:任意输入规模的最大运行次数(上界);
  2. 平均情况:任意输入规模的期望运行次数;
  3. 最好情况:任意输入规模的最小运行次数(下界)。

但是,在实际中一般关注的是算法的最坏运行情况。

案例一

void Func1(int N) 
{
	 int count = 0;

	 for (int k = 0; k < 2 * N ; ++ k)
	 {
 		++count;
 	 }

	 int M = 10;

 	 while (M--)
 	 {
 		++count;
 	 }

 	 printf("%d\n", count);
}

分析:

总体时间复杂度为:O(2N + 10)

采用大O渐进法得时间复杂度为:O(N)

案例二:

void Func2(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(N+M)

采用大O渐进法得时间复杂度为:O(N+M)

案例三:

void Func3(int N) 
{
 	int count = 0;

 	for (int k = 0; k < 100; ++k)
 	{
 		++count;
 	}

	 printf("%d\n", count);
 }

分析:

总体时间复杂度为:O(100)

采用大O渐进法得时间复杂度为:O(1)

案例四:

const char * strchr ( const char * str, int character );

分析:

strchr函数是一个字符查找函数,用以实现在字符串str中查找字符character。

最好的情况:第一个元素即为要查找的元素,时间复杂度为O(1)。最坏的情况:最后一个元素才为要查找的元素或者遍历整个字符串之后查找失败,时间复杂度为O(N)。而时间复杂度一般看的是最坏情况,因此总体时间复杂度为:O(N)

采用大O渐进法得时间复杂度为:O(N)

案例五:

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;
 	}

}	

分析:

冒泡排序:比较相邻的两个元素,数值小的元素慢慢的向前移动,数值大的元素慢慢的向后移动。

最好的情况:整个数组是有序排列的,只需要比较n-1次,而不需要移动元素,此时的时间复杂度为:O(N)。最坏的情况:整个数组是由大到小排列的,总共需要n-1轮循环,则第一轮循环需要比较n-1次,第二轮循环需要比较n-2次,第三轮循环需要比较n-3次,...,第n-1轮循环需要比较1次。则总共比较的次数为(n-1)*n/2次。而时间复杂度一般看的是最坏情况,因此总体时间复杂度为:O((N - 1)*N/2)

采用大O渐进法得时间复杂度为:O(N^2)

案例六:

int BinarySearch(int* a, int n, int x) 
{
	assert(a);

 	int begin = 0;
 	int end = n-1;

 	while (begin < end)
 	{
 		int mid = begin + ((end-begin)>>1);
 		if (a[mid] < x)
 			begin = mid+1;
 		else if (a[mid] > x)
 			end = mid;
 		else
 			return mid;
 	}

 	return -1;
}

分析:

二分查找法:二分查找法是一种高效的查找算法,用于在有序数组或有序列表中快速定位目标元素的位置。它通过将目标值与数组中间元素进行比较,从而将查找范围缩小一半,不断迭代直到找到目标元素或确定目标元素不存在。

最好的情况:查找一次变查找到所要查找的元素,此时的时间复杂度为O(1)。最坏的情况:当数组中只剩下最后一个元素,或者查找失败,由于二分查找法每次范围都是缩小一半,则:n / 2 / 2 / 2… /2 = 1。假设查找x次,则2^x=n,两边取对数得:x=log2^n,所以总共比较的次数为次log2^n。而时间复杂度一般看的是最坏情况,因此总体时间复杂度为:O(log2^N)

采用大O渐进法得时间复杂度为:O(log2^N)

案例七:

long long Fac(size_t N) 
{
	if(1 == N)
 		return 1;

 	return Fac(N-1)*N; 
}

分析:

数据结构初阶--绪论_第4张图片

在计算阶乘递归Fac时,可以发现函数只递归调用了N-1次。

总体时间复杂度为:O(N-1)

采用大O渐进法得时间复杂度为:O(N)

案例八:

long long Fib(size_t N) 
{
 	if(N < 3)
 		return 1;

 	return Fib(N-1) + Fib(N-2);
}

分析:

数据结构初阶--绪论_第5张图片

在计算斐波那契递归Fib时,可以发现函数递归呈现指数式增长。

将2^0+2^1+2^2+2^3+…2^(N-2)进行累加得:2^(N-1) - 1,此数也即为函数递归调用的次数。

总体时间复杂度为:O(2^(N-1) - 1)

采用大O渐进法得时间复杂度为:O(2^N)

四.算法的空间复杂度

数据结构初阶--绪论_第6张图片

空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度。空间复杂度算的是变量的个数。它的计算规则基本和时间复杂度类似,也使用大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是不需要占用空间的,因为函数运行时所需要的栈空间(存储参数,局部变量,一些寄存器信息等)在编译期间已经确定好了。在函数中,新增的变量其实是end和i,以及exchange。

总体空间复杂度为:O(3)

采用大O渐进法得空间复杂度为:O(1)

案例二:

long long* Fibonacci(size_t n) 
{
 	if(n==0)
 		return NULL;
 
 	long long * fibArray = (long long *)malloc((n+1) * sizeof(long long));

 	fibArray[0] = 0;
 	fibArray[1] = 1;
 	for (int i = 2; i <= n ; ++i)
 	{
 		fibArray[i] = fibArray[i - 1] + fibArray[i - 2];
 	}

 	return fibArray;
 }

分析:

该函数返回的是斐波那契数列的前n项,为了存放该数列的前n项,需要调用malloc函数来动态开辟n+1个空间来存放。

总体空间复杂度为:O(n+1)

采用大O渐进法得空间复杂度为:O(N)

案例三:

long long Fib(size_t N) 
{
 	if(N < 3)
 		return 1;

 	return Fib(N-1) + Fib(N-2);
}

分析:

在解答此题之前需要明白的一点是:时间是累计的,空间是不累计的,可以重复利用。在绝大多数情况下,函数递归调用时的空间复杂度=递归调用的深度。

Fib(N-1)和Fib(N-2)在递归的过程中,存在很多相同的部分,在这些相同的部分,它们是共用同一块内存空间的。

采用大O渐进法得空间复杂度为:O(N)

案例四:

long long Fac(size_t N)
{
	if(N == 1)
		return 1;

	return Fac(N-1)*N;
}

分析:

在绝大多数情况下,函数递归调用时的空间复杂度=递归调用的深度。该函数递归调用了N次,开辟了N个栈帧,每个栈帧使用了常量个空间。

采用大O渐进法得空间复杂度为:O(N)

五.复杂度练习

题一:消失的数字

描述:

数组nums包含从0到n的所有整数,但其中缺了一个。请编写代码找出那个缺失的整数。你有办法在O(n)时间内完成吗?

分析:

法一:
使用malloc开辟一个大小为N+1的数组,并初始化为-1。然后遍历数组nums中的各个数字,这个数字是多少就写到新开辟的数组的对应的位置上去,最后再遍历一遍新开辟的数组,哪个位置是-1,这个位置的下标就是缺失的数字。                                                                                                              时间复杂度为:O(N),空间复杂度为:O(N)。

法二:
用异或的思想。设x=0,将x跟数组中的这些数据都异或一遍,然后再跟0-N之间的数字异或一遍,最后得到的x就是缺失的数字                                                                                                                        时间复杂度为:O(N)
注意:一个数与自身异或,总是为0,一个数与0异或,总是其自身。

实现:

int missingNumber(int* nums, int numsSize)
{
	int x = 0;

	for (int i = 0; i < numsSize; i++)
	{
		x ^= nums[i];
	}

	for (int j = 0; j < numsSize + 1; j++)
	{
		x ^= j;
	}

	return x;
}

法三:

排序+二分查找相结合。
排序算法的时间复杂度:冒泡O(N^2) ,快排O(N*logN)
二分查找的时间复杂度:O(log2^N)

法四:

使用求和公式。如果有n个数,则0+1+2…+n,最后整体再减去数组中的值的累加就是缺失的数。                                                                                                                                                             时间复杂度:O(N)

实现:

int missingNumber(int* nums, int numsSize)
{
	int i = 0;
	
	int sum = 0;
	for (i = 0; i < numsSize + 1; i++)
	{
		sum += i;
	}
	
	for (i = 0; i < numsSize; i++)
	{
		sum -= nums[i];
	}

	return sum;
}

题二:旋转数组

描述:

给你一个数组,将数组中的元素向右轮转k个位置,其中k是非负数。使用时间复杂度为O(N),空间复杂度为O(1)的原地算法解决这个问题。

分析:

法一:

设置变量tmp。将最右边的元素拷贝到tmp中,然后将数组中的值依次右移动一位,再把tmp中的内容存放到数组起始位置;重复上述操作k次,就可以实现数组的旋转。                                               时间复杂度为O(N^2),空间复杂度为O(1)。

法二:

以空间换时间。首先开辟一个新数组,用以存放原数组中的元素。然后将原数组中的后k个元素保持起始序列依次存放到新数组的开始k个位置。其次将原数组剩余的n - k个元素保持起始序列依次存放到新数组的后n-k个位置。最后再将新数组的元素依次拷贝到原数组中。                                        时间复杂度为O(N),空间复杂度为O(N)。

实现:

void rotate(int* nums, int numsSize, int k)
{
	int n = numsSize;
	int tmp[n];//变长数组【C99】

	//首先对k做一个取模操作,防止数组访问越界
	k %= n;

	//将后k个数字移动到前面
	int j = 0;
	for (int i = n - k; i < n; ++i)
	{
		tmp[j++] = nums[i];
	}

	//将前n - k个数字移动到后面
	for (int i = 0; i < n - k; ++i)
	{
		tmp[j++] = nums[i];
	}

	//将移动完的数组再拷贝回原数组
	for (int z = 0; z < n; ++z)
	{
		nums[z] = tmp[z];
	}
		
}

法三:

三步翻转法。首先将前n-k个元素进行逆置,然后将后k个元素进行逆置,最后再将n个元素进行整体逆置。                                                                                                                                                           时间复杂度为O(N),空间复杂度为O(1)。

实现:

void reverse(int* nums, int left, int right)
{
	while (left < right)
	{
		int tmp = nums[left];
		nums[left] = nums[right];
		nums[right] = tmp;
		++left;
		--right;
	}
}

void rotate(int* nums, int numsSize, int k)
{
	k %= numsSize;
	reverse(nums, numsSize - k, numsSize - 1);
	reverse(nums, 0, numsSize - k - 1);
	reverse(nums, 0, numsSize - 1);
}

你可能感兴趣的:(数据结构,数据结构)