目录
( 2 )什么是抽象数据类型( Abstract Data Type ADT)
前言
内存中存储管理数据的结构——数据结构
数据库和数据结构的区别?
- 本质都是存储管理数据
- 而数据结构是在内存中存储管理数据的——内存是带电存储
- 数据库是在磁盘中存储管理数据的
算法,就是对数据按要求进行某种处理,查找,排序等。
算法和数据结构的关系:你中有我,我中有你
1. 什么是数据结构?
数据作为计算机加工处理的对象,如何在计算机中表示和存储数据是计算机科学研究的只要内容之一,更是计算机技术需要解决的关键问题之一。
程序=数据结构+算法
数据是计算机的信息,是计算机处理的主要对象。科学计算、数据处理、过程控制、文件存储、数据库技术等,都涉及对数据进行加工处理的过程。因此,一个好的程序,要有好的合适的数据结构以及好的算法。
从问题到程序——计算机初期,人们的目的主要是处理数值计算问题;如今,计算机在处理非数值计算性问题上占用了90%。因此,解决这类问题的关键不再是数学分析和计算方法,而是设计出合适的数据结构,一边有效的解决问题。
( 1 )有关概念和术语
1.数据(Data):数据是信息的载体,他能够被计算机识别、存储和处理。数据是计算机程序加工的原料,应用程序能处理各种各样的数据,包括数值和非数值数据。数值数据是一些整数、实数或复数;非数值数据包括字符、文字、图形、语音等。
2.数据元素(Data Element):数据元素是数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理。一个数据元素可由若干个数据项(Data Item)组成。在不同的条件下,数据元素又可称为数据元素、节点、顶点、记录等。如学生信息检索中学生信息表中的一个记录、教学计划编排问题中的一个顶点等,都被称为一个数据元素。
3.数据项(Data Item):数据项指不可分割、具有独立意义的最小数据单位,数据项有时也称为字段(Field)或域。如学籍管理系统中学生信息表的每一个数据元素就是一个学生记录。它包括学生的学号、姓名、性别、籍贯、出生年月、成绩等数据项。这些数据项可以分为两种:一种叫做初等项,如学生的性别、籍贯等,这些数据项是数据处理时不能分割的最小单位;另外一种叫做组合项,如学生的成绩,它可以再划分为数学、物理、化学等更小的项。
4.数据结构(Data Structure):是指相互之间存在着一种或多种关系的数据元素的集合。数据结构包括逻辑结构和物理结构。
数据结构的两个要素:一是数据元素,二是数据元素之间的关系
数据结构包括逻辑结构和物理结构
因此,数据结构通常采用一个二元组来表示:
Data_structure = ( D , R )
其中,D是数据元素的集合,R是D中数据元素之间关系的集合
5.数据类型(Data Type):数据类型是和数据结构密切相关的一个概念,在高级程序设计语言中用以限制变量取值范围和可能进行的运算的总和称为数据类型。因此,所谓的数据类型,一是限定了数据的取值范围(实际上与存储形式有关);二是规定了数据能够进行的一组运算。
数据类型可以分为两类:一类是非结构的原子类型,原子类型的值是不可再分解的,如C语言中的基本型;另一种是结构类型,它的成分可以有多个数据类型组成,并可以分解。结构类型的成分可以是非结构的,也可以是结构的。例如:数组的值是由若干个分量组成,每个分量可以是整数等的基本类型,也可以是数组等的结构类型。
7.抽象数据类型:泛指除基本数据类型以外的数据类型。基本数据类型被认做是最基本地,不可再划分的数据,一般就是整形、浮点型、以及字符型。抽象数据类型是由若干基本数据类型归并之后形成的一种新的数据类型,这种类型由用户定义,功能操作比基本数据类型更多,一般包括结构体和类。其实说白了,抽象数据类型就是把一些有一定关联的基本数据类型打包,然后当做新的数据类型使用。
( 2 )什么是抽象数据类型( Abstract Data Type ADT)
C/C++有固有数据类型,比如int,float,double。int a; 就声明且定义出一个int型变量(或者叫数据对象);但光有这些固有的数据类型不能满足编程的可读、可复用、可维护性的要求。
比如想要处理一个现实中“学生小明”的对象,如果能有一个名叫“Student”的“学生”类型(好比“int”类型),通过像Student Xiaoming; 这样的语句,就可以定义出一个名为Xiaoming的学生类型对象(就好比“int a;”定义出一个名为a的int整型变量那样),那就好办了。
同样的,如果能实现一个名叫“Stack”的栈数据类型,名叫“List”的链表数据类型,名叫“Car”的汽车数据类型,名叫“Plane”的飞机数据类型……,就很方便了。用C++的话说来就是Class Student{……}; Student Xiaoming; Class Stack{……}; Stack my_stack; Class Plane{……}; Plane Boing747; ,
如此我们自定义出想要的数据类型,然后使用它。
用面向对象的语言和语法,更容易理解抽象数据类型,就像上面写的那样。“抽象、封装、继承、多态”是面向对象的4个特点,抽象就是把同一数据类型的共有特征概括出来。
一个数据对象有四个属性:V(值),A(地址),N(名称),T(类型)。
在程序编译完成后,可执行程序里面产生了信息缺失:程序中的数据对象只有地址和值,没有数据类型、数据名称、数据意义等信息了。所以抽象完成在编写程序的时候。抽象得出的数据类型的特点包含两个方面:属性和方法。
拿“Student”学生类型来说,姓名,性别,年级,班级,成绩等就是学生类型的“属性”,可以由各种变量表示;努力学习,升学,考试,翘课,挂科,被加分,被表扬,被扣分,被处分等就是学生类型的“方法”,可以由各种函数表示。
方法可以对属性进行操作,比如“被加分”会修改“成绩”这个属性。
封装就是通过一定的语法形式,把抽象出来的属性和方法“捆绑在一起”,在形式上写成一个整体,使人从形式上就能看出两者的紧密关系,这个过程叫封装。
通过封装,还可以将部分属性和方法隐藏起来,对外只留一定的接口(函数),实现“信息隐藏”。封装可以理解为抽象的具体表现形式。
C语言实现抽象数据类型不如面向对象语言来的方便,不过抽象和封装还是能实现的。
2. 什么是算法
算法(algorithm):就是定义良好的计算过程,他取一个或一组的值为输入,并产生出一个或一组值作为输出。简单来说算法就是一系列的计算步骤,用来将输入数据转化成输出结果。
mark:我们可以把所有的算法想象为一本“菜谱”,特定的算法比如菜谱中的的一道“老醋花生米”的制作流程,只要按照菜谱的要求制作老醋花生米,那么谁都可以做出一道好吃的老醋花生米。so,这个做菜的步骤就可以理解为:“解决问题的步骤”
( 1 )算法的意义
假设计算机无限快,并且计算机存储容器是免费的,我们还需要各种乱七八糟的算法吗?如果计算机无限快,那么对于某一个问题来说,任何一个都可以解决他的正确方法都可以的!
当然,计算机可以做到很快,但是不能做到无限快,存储也可以很便宜但是不能做到免费。
那么问题就来了效率:解决同一个问题的各种不同算法的效率常常相差非常大,这种效率上的差距的影响往往比硬件和软件方面的差距还要大。
( 2 )如何选择算法
第一首先要保证算法的正确性
一个算法对其每一个输入的实例,都能输出正确的结果并停止,则称它是正确的,我们说一个正确的算法解决了给定的计算问题。不正确的算法对于某些输入来说,可能根本不会停止,或者停止时给出的不是预期的结果。然而,与人们对不正确算法的看法想反,如果这些算法的错误率可以得到控制的话,它们有时候也是有用的。但是一般而言,我们还是仅关注正确的算法!
第二分析算法的时间复杂度
算法的时间复杂度反映了程序执行时间随输入规模增长而增长的量级,在很大程度上能很好反映出算法的好坏。
( 3 )算法的复杂度
算法在编写成可执行程序后,运行时需要耗费时间资源和空间(内存)资源 。
因此衡量一个算法的好坏,一般是从时间和空间两个维度来衡量的,即时间复杂度和空间复杂度。
时间复杂度主要衡量一个算法的运行快慢,而空间复杂度主要衡量一个算法运行所需要的额外空间。
在计算机发展的早期,计算机的存储容量很小。所以对空间复杂度很是在乎。但是经过计算机行业的迅速发展,计 算机的存储容量已经达到了很高的程度。所以我们如今已经不需要再特别关注一个算法的空间复杂度。
3. 什么是时间复杂度
( 1 )时间复杂度的概念
时间复杂度的定义:在计算机科学中,算法的时间复杂度是一个函数,它定量描述了该算法的运行时间。
一 个算法执行所耗费的时间,从理论上说,是不能算出来的,只有你把你的程序放在机器上跑起来,才能知道。但是我们需要每个算法都上机测试吗?是可以都上机测试,但是这很麻烦,所以才有了时间复杂度这个 分析方式。
一个算法所花费的时间与其中语句的执行次数成正比例,算法中的基本操作的执行次数,为算法的时间复杂度。
即:找到某条基本语句与问题规模N之间的数学表达式,就是算出了该算法的时间复杂度。
// 请计算一下Func1中++count语句总共执行了多少次?
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N; ++i)
{
for (int j = 0; j < N; ++j)
{
++count;
}
}
for (int k = 0; k < 2 * N; ++k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}
实际中我们计算时间复杂度时,我们其实并不一定要计算精确的执行次数,而只需要大概执行次数,那么这里我们使用大O的渐进表示法。
( 2 )大O的渐进表示法
大O符号(Big O notation):是用于描述函数渐进行为的数学符号。(大概估算,方便比较)
推导大O阶方法:
- 用常数1取代运行时间中的所有加法常数。
- 在修改后的运行次数函数中,只保留最高阶项。
- 如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。
使用大O的渐进表示法以后,Func1的时间复杂度为:
O(N²)
- N = 10 F(N) = 100
- N = 100 F(N) = 10000
- N = 1000 F(N) = 1000000
通过上面我们会发现大O的渐进表示法去掉了那些对结果影响不大的项,简洁明了的表示出了执行次数。
另外有些算法的时间复杂度存在最好、平均和最坏情况:
最坏情况:任意输入规模的最大运行次数(上界)
平均情况:任意输入规模的期望运行次数
最好情况:任意输入规模的最小运行次数(下界)
例如:在一个长度为N数组中搜索一个数据x
最好情况:1次找到
最坏情况:N次找到
平均情况:N/2次找到
底线思维:
在实际中一般情况关注的是算法的最坏运行情况,所以数组中搜索数据时间复杂度为O(N)
( 3 )常见时间复杂度计算举例 —— log N
用换底公式忽略系数:log n = lg n / lg 2
实例1:
// 计算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);
}
//O(N)
实例2:
// 计算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(N+M) 不知道N和M的大小,如果告知N远大于M,O(N),反之,O(M),如果M和N一样大,O(N)或者O(M)
实例3:
// 计算Func4的时间复杂度?
void Func4(int N)
{
int count = 0;
for (int k = 0; k < 100; ++k)
{
++count;
}
printf("%d\n", count);
}
//O(1)_用常数1取代所有运行时间中的加法常数——不是表示1次,而是表示常数次
实例4:
// 计算strchr的时间复杂度?
//strchr() 用于查找字符串中的一个字符,并返回该字符在字符串中第一次出现的位置。
const char* strchr(const char* str, int character);
//O(N)
实例5:
// 计算BubbleSort的时间复杂度?
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;
}
}
//准确的F(N) = N-1+N-2+N-3+N-4+..+2+1
// 等差数列
// F(N) = N*(N-1)/2
// 算时间复杂度不能去数循环,这个不准确,一定要看算法思想去计算
//O(N^2)
//最好的情况O(N),至少比较N-1
实例6:
// 计算BinarySearch的时间复杂度?
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(1)
//最坏——找不到,或者最后一次找到 1*2*2*..*2 = N
//折半了多少次就除了多少个二
//除了多少个二就找了多少次
//假设折半查找了x次
//2^x = N
//log以二为底N的对数
实例7:
// 计算阶乘递归Fac的时间复杂度?
long long Fac(size_t N)
{
if (0 == N)
return 1;
return Fac(N - 1) * N;
}
//O(N)
实例8:
// 计算斐波那契递归Fib的时间复杂度?
long long Fib(size_t N)
{
if (N < 3)
return 1;
return Fib(N - 1) + Fib(N - 2);
}
//O(N^2)
4. 什么是空间复杂度
( 1 )空间复杂度的概念
空间复杂度也是一个数学表达式,是对一个算法在运行过程中临时占用存储空间大小的量度 。
空间复杂度不是程序占用了多少bytes的空间,因为这个也没太大意义,所以空间复杂度算的是变量的个数。
空间复杂度计算规则基本跟实践复杂度类似,也使用大O渐进表示法。
注意:函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
( 2 )常见空间复杂度计算举例
实例1:
// 计算BubbleSort的空间复杂度?
#include<assert.h>
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;
}
}
//额外的开辟的临时空间,exchange,end,i,Swap四个变量
//空间复杂度度O(1)
实例2:
// 计算Fibonacci的空间复杂度?
// 返回斐波那契数列的前n项
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 + 1
//空间复杂度为O(N)
实例3:
// 计算阶乘递归Fac的空间复杂度?
long long Fac(size_t N)
{
if (N == 0)
return 1;
return Fac(N - 1) * N;
}
//递归调用了N次,开辟了N个栈帧,每个栈帧使用了常数个空间。
//O(N)
( 3 )斐波那契递归空间复杂度 O(N)
5. 常见复杂度对比
一般算法常见的复杂度如下:
- O(1) — 常数复杂度 ——5201314
- O(log n) — 对数复杂度——3log(2)n + 4
- O(n) — 线性复杂度——3n+4
- O(n log n) — 对数线性复杂度——2n + 3n log(2)n + 14
- O(nᵏ) — 多项式复杂度——3n^2 + 4n + 5
- O(kⁿ) — 指数复杂度——2^n
- O(n!) — 阶乘复杂度
6. 复杂度的oj练习
( 1 )消失的数字OJ链接:
面试题 17.04. 消失的数字 - 力扣(LeetCode)https://leetcode.cn/problems/missing-number-lcci/
int missingNumber(int* nums, int numsSize){
int x = 0;
x = (numsSize + 0)*(numsSize+1)/2;
for(int i = 0;i<numsSize;i++)
{
x-=nums[i];
}
return x;
}
//思路4
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;
}
( 2 )旋转数组OJ链接:
189. 轮转数组 - 力扣(LeetCode)https://leetcode.cn/problems/rotate-array/
//暴力做法
void rotate(int* nums, int numsSize, int k){
k=k%numsSize;
int temp;
while(k--)
{
temp=nums[numsSize-1];
for(int i=numsSize-1;i>0;i--)
{
nums[i]=nums[i-1];
}
nums[0]=temp;
}
void rotate(int* nums, int numsSize, int k){
reserver(nums,0,numsSize-1);
reserver(nums,0,k%numsSize-1);
reserver(nums,k%numsSize,numsSize-1);
}
void reserver(int* nums ,int head,int tail)
{
int tmp = 0;
while(head<tail)
{
tmp = *(nums+head);
*(nums+head) = *(nums+tail);
*(nums+tail) = tmp;
head++;
tail--;
}
}
void rotate(int* nums, int numsSize, int k)
{
k = k % numsSize;
int i, j;
int a[100000];
j = numsSize - k;
for (i = 0; i < k; i++)
{
a[i] = nums[j++];
}
j = 0;
for (; i < numsSize; i++)
{
a[i] = nums[j++];
}
for (i = 0; i < numsSize; i++)
{
nums[i] = a[i];
}
}