目录
数据结构 + 算法 = 程序
数据,被计算机处理的符号或操作的对象;
数据元素,数据结构中基本的独立单元,可由若干个数据项组成;
数据对象,系统数据元素的集合,是数据的子集,通常简称数据对象为数据;
一,数据结构(Data Structure)
- 是计算机存储、组织数据的方式;
- 是数据元素的集合,数据元素相互之间存在一种或多种特定关系;
- 不同种类数据结构适用于不同种类应用,合适的数据结构可带来高效运行和存储;
数据的逻辑结构
反应数据元素间的逻辑关系(逻辑关系即数据元素间的前后关系);
- 集合:数据元素间“同属一个集合” ,但元素间相互孤立;
- 线性结构:数据结构中的元素存在一对一的相互关系;
- 仅有一个起始结点和终端结点,所有结点最多一个前驱和后驱结点;
- 包括数组、链表,及串string(特殊线性表),栈stack(特殊线性表)、队列queue(特殊线性表);
- 树形结构:数据结构中的元素存在一对多的相互关系;
- 包括二叉树(最简单)、多叉树、字典树等;
- 图形结构:数据结构中的元素存在多对多的相互关系;
- 复杂的非线性结构,由顶点(或结点)和边组成;
- 包括无向图、有向图、连通图等;
按逻辑简单分类:线性结构、非线性结构;
数据的存储结构
在计算机存储空间中的存放形式;
- 顺序存储、链式存储、索引存储和哈希存储等;
顺序存储结构
- 数据元素存放在连续地址,其逻辑关系可通过存储地址直接反映;
- 逻辑相邻的元素,在物理地址上也相邻;
- 优:简单易理解,且占用空间最少,可随机存取;
- 缺:需连续存储空间,且要事先分配,对一些操作时间效率较低(如移动、删除等);
链式存储结构
- 数据元素可存放任意地址,可连续也可不连续;
- 数据元素间的逻辑关系是通过指针间接反映;
- 逻辑相邻的元素,在物理地址上不一定相邻;
- 优:存储空间不必事先分配,可临时申请不浪费空间,一些操作时间效率较高(插入、移动、删除)
- 缺:需额外存储指针,比顺序结构空间开销大,只可顺序存取;
索引存储结构
- 存储数据元素时,附加建立索引表;
- 索引表中每一项称为索引项(形式为:关键字,地址);
- 优:检索速度快;
- 缺:需额外存储索引表,在增加、删除数据时需修改索引表;
散列存储结构
- 根据元素关键字直接计算出存储地址;
- 优:检索、增加、删除操作快;
- 缺:散列函数可能出现存储单元冲突;
八类常见数据结构
-
线性数据结构
-
数组(Array)
-
链表(Linked List)
-
栈(Stack)
-
队列(Queue)
-
-
非线性数据结构
-
树(Tree)
-
堆(Heap)
-
散列表(Hash)
-
图(Graph)
-
二,算法(Algorithm)
- 是对特定问题求解步骤的一种描述,是一系列指令的集合,每一条指令表示一个或多个操作;
- 表达某一类计算问题的一般方法,对任何一个输入,可按步骤一步一步计算最终产生输出;
算法特征
- 有穷性(Finiteness),指算法必须能在执行有限个步骤之后终止;
- 确切性(Definiteness),算法的每一步骤必须有确切的定义,不能产生二义性;
- 可行性(Effectiveness),执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成(也称之为有效性);
- 输入项(Input),一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定出了初始条件;
- 输出项(Output),一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
数据对象的运算和操作
算法要求
- 正确性
- 可读性
- 健壮性
- 简单性
- 时间效率高而且空间使用率低
算法效率
- 同一问题可用不同算法解决,而一个算法的质量优劣将影响到算法乃至程序的效率;
- 衡量一个算法的好坏,时间复杂度和空间复杂度;
//问题规模N
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);
}
以上Func1 执行的基本操作次数:
时间复杂度
- 计算基本运算的执行次数,t(n) = O(f(n));
- 表示随问题规模n增大,算法执行时间的增长率和f(n)的增长率正相关;
- 实际中只需计算大概的执行次数,即大O渐进表示法;
大O渐进表示法(即去掉了那些对结果影响不大的项)
- 用常数1取代运行时间中的所有加法常数;
- 在修改后的运行次数函数中,只保留最高阶项;
- 如最高阶项存在且不是1,则去除与这个项相乘的常数;
使用大O的渐进表示法,Func1的时间复杂度为:
常数阶:O(1)
- 基本运算次数与问题规模无关;
- O(1)不代表1次运算;
常用的算法时间复杂度的关系:
- O(1) < O(logn) < O(n) < O(nlogn) < O() < O() < O()
注:另外有些算法的时间复杂度存在最好、平均和最坏情况,在实际中一般情况关注的是算法的最坏运行情况;
空间复杂度
- 算法在运行过程中临时占用的空间的度量,一般也是问题规模n的函数s(n) = O(g(n));
- 函数运行时所需要的栈空间(存储参数、局部变量、一些寄存器信息等)在编译期间已经确定好了,因此空间复杂度主要通过函数在运行时候显式申请的额外空间来确定。
- 空间复杂度不是程序占用了多少空间,算的是变量的个数; 空间复杂度计算规则基本跟时间复杂度类似,也使用大O渐进表示法。
- 算法的时间复杂度和空间复杂度相互影响,好的时间复杂度也可能会占用较多的存储空间;
方法
-
递推法
-
递归法
-
穷举法(暴力破解法)
-
贪心算法
-
分治法
-
动态规划法
-
迭代法
-
分支界限法
-
回溯法
三,案例
冒泡排序
时间复杂度
- 最好O(N),平均O(),最差O();
- N+(N-1)+(N-2)+...+1 (最后一次判断以不满足条件)
空间复杂度
- 使用了常数个额外空间,O(1);
//冒泡排序
void BubbleSort(int* parr, int sz)
{
assert(parr);
int i = 0;
for (i; i < sz - 1; i++)
{
int j = 0;
int flag = 0;
for (j; j < sz - 1 - i; j++)
{
if (parr[j] > parr[j + 1])
{
int tmp = parr[j];
parr[j] = parr[j + 1];
parr[j + 1] = tmp;
flag = 1;
}
}
if (flag == 0)
break;
}
}
二分查找
时间复杂度
- 最好O(1),最差O(logN);
- 1 = N/
空间复杂度
- 使用了常数个额外空间,O(1);
//二分查找,要求有序数列
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(N) ;
- 时间复杂度 = 递归次数的累加;
空间复杂度
- O(N);
long long Fac(size_t N)
{
if(1 == N)
return 1;
return Fac(N-1)*N;
}
斐波那契递归
时间复杂度
- O() ;
- 2^0+2^1+2^2+...+2^(n-3)+2^(n-2) = 2^(n-1)-1
空间复杂度
- O(N)
long long Fib(size_t N)
{
if(N < 3)
return 1;
return Fib(N-1) + Fib(N-2);
}