数据结构小白入门

数据结构指一组相互之间存在一种或多种特定关系的数据元素的集合,
当我们需要在计算机中存储这些数据时,还涉及到数据的,组织方式,在计算机中的存储方式,以及定义在该数据上的一组操作;

  • 一组数据相互之间有某种关系
  • 组织方式
  • 存储方式
  • 以及可对其进行的一组操作
理解:

我们学习的最终目的是要在计算机中存储一组数据,但是不得不先考虑数据的组织方式,在计算机中的存储方式,以及可以对这些数据进行的一组操作,当然了既然是一组数据必然表明了这写数据之间是存在想换的关联关系的;关系可能还会有多种;

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。
例如:

一组数据:12345

组织方式:从小到大

存储方式:可使用线性存储结构

操作:要取出最大的一个

数据结构研究方向

问题:

  • 机外处理
  • 处理要求

建模:

  • 逻辑结构
  • 基本运算

实现:

  • 存储结构
  • 算法

基本术语

数据(Data):

​ 所有能被计算机处理的符号的集合

数据元素(DataElement):

​ 是数据集合中的一个 个体,即数据的基本单位

数据项(DataItem):

​ 数据元素常常可分为若干个数据项,数据项是数据具有意义的最小单位

组织数据的三个层次:

数据(表)->数据元素(行)->数据项(字段)

实际问题中的数据成为原始数据

逻辑结构(LogicalStructure)

​ 数据元素之间的结构关系,如从小到大/一对一/一对多

物理结构(PhysicalStructure)

​ 也会叫做存储结构,指数据在计算机内的表示,逻辑结构在计算机中的具体实现

逻辑结构

常见的逻辑结构如下:

集合:

数据元素属于同一个集合,表示为R{}; 数据之间不存在特定关系

组织结构松散,任意两节点之间都没有邻接关系

线性:

除了起始节点d1和终端阶段dn外,每个节点都有一个前驱和一个后继,表示为R={d1,d2...dn},数据之间存在前后顺序关系

各节点按逻辑关系排列,形成一条'链'

树状:

每个元素最多有一个前驱,可以有多个后继,表示为(D,{R}),就像一个树干长了多个树枝

具备分支,层次特性,上层节点可以和下层多个节点相邻接,但是下层节点只能和一个上层节点邻接

图状:

任何两个元素之间都可以相邻接,表示为(D,{R})

注意:

逻辑结构

  • 与元素本身的形式,内容,无关

  • 元素的相对位置,无关

  • 与包含的节点个数,无关

存储结构

存储结构由 存储节点(每个存储节点存放一个数据元素)节点之间的逻辑关系共同组成

反过来说,一个完整的存储结构必须可以存储数据元素,以及元素之间的逻辑关系

存储结构分类 (缺图)

  • 顺序存储

    使用索引(相对起始位置)来表示数据的逻辑结构,数据被存储在一组连续的存储单元中

    特点:

    • 需预先分配长度,
    • 插入和删除慢,需要移动其他元素
    • 存取数据快捷,属于随机存储结构(可通过索引直接访问任意位置数据)
  • 链式存储

    借助元素地址指针表示数据的逻辑结构,每个元素都会包含指向下一个元素的指针

    这种结构需要在节点上附加一个指针项,指出后继节点的位置,即每个节点存储单元包含两个部分:[数据项,指针项]

    特点:

    • 动态分配内容,不需要预先分配内存
    • 插入删除快捷,不需要移动其他元素
    • 非随机存取结构(获取数据必须遍历前面的所有节点)
  • 索引存储(Map是否属于索引结构 很疑惑?)

    借助索引表来指示数据元素的存储位置

    索引表中包含了所有数据元素的地址,查询索引表能够快速的定位到需要的数据

    特点:

    • 索引是一份独立于实际存放数据,的数据结构(就像书的目录都在正文前面)
    • 索引需要占用额外的存储空间
    • 当实际数据发生改变时需要重建索引
    • 查询数据快
    • 插入修改,删除慢
  • 散列存储(哈希表)

    通过散列函数计算得出元素的位置

    特点:

    • 在散列函数不变时,相同数据会得出相同的位置
    • 存入顺序和取出顺序通常不一致
    • 无法完成随机存取(指定获取某个元素)

顺序和链式是最基本的也是最常用的存储结构,需要重点掌握,包括各自的优缺点,使用场景等

链式存储结构可实现树结构(逻辑结构)

运算

运算指的是某种逻辑结构上可以进行的操作;

运算分为两类:

  • 加工型运算

    会改变原逻辑结构的内容,顺序,个数等的操作

  • 引用型运行

    与加工型运算相反

常见运算:

建立,查找,读取,插入,删除

加工型:建立,插入,删除

引用型:读取,查找

算法

算法字面意思,计算方法;

算法规定了求解给定类型问题所需的所有处理步骤以及执行顺序,使得问题能在有限时间内机械的求解,一个算法就是对特定问题求解步骤的一种描述,再具体一点,算法是一段有穷的指令序列;算法必须能使用某种语言描述;

例如:

计算1到5的和 ,这个需求,如何来实现,第一步做什么,第二步做什么,整个计算步骤和执行顺序统称为算法,如果最终能够在有限的步骤下求出正确的和,那这就是一个合格的算法;

算法的特点:

  • 有穷性

    算法必须在执行有穷步后结束

  • 确定性

    算法的每一个步骤都必须是明确定义的,

  • 可行性

    算法中的每一步都是可以通过已实现的操作来完成的

  • 输入

    一个算法具备0或多个输入

  • 输出

    一个算法有一个或多个输出,它们与输入有着特定的关系

算法与程序的区别,算法只是一种描述,可以使用任何语言,但是通常不能直接被计算机运行,而程序则是算法的具体实现,使用某种计算机语言;

算法设计应满足的要求

  • 正确性:对于合法的输入产生符合要求的输出

  • 易读性:算法应该尽可能易读,便于交流,这也是保证正确性的前提(注释可提高易读性)

  • 健壮性:当输入非法数据时,算法可作出适当反应而不至于崩溃(例如输出错误原因);

  • 时空性:指的是算法的时间复杂度和空间复杂度,算法分析主要也是分析算法的时间复杂度和空间复杂的,其目的是提高算法的效率;

算法分析

解决同一问题的算法可能有多种,我们希望从中选出最优的算法,效率高的,占用空间小的,为此我们就需要对算法进行评估和分析;

通常评估算法根据两个度量

  • 时间复杂度:算法运行完成所需的总步数(标准操作),通常是问题规模的函数

  • 空间复杂度:算法执行时所占用的存储空间,通常是问题规模的函数

确定算法的计算量

  • 合理选择一种或几种操作作为'标准操作',无特殊说明默认以赋值操作作为标准操作;
  • 确定算法共执行多少次标准操作,并将此次数规定为算法的计算量
  • 以算法在所有时输入下的计算量最大值作为算法的最坏情况时间复杂度
  • 以算法在所有时输入下的计算量最小值作为算法的最好情况时间复杂度
  • 以算法在所有时输入下的计算量平均值作为算法的平均情况时间复杂度
  • 最坏/平均情况时间复杂度都可作为时间复杂度,通常以最坏情况衡量;

注意:时间复杂度通常以量级来衡量,也就是说不需要精确的计算到底执行了几步,而是得出其计算量的数量级即可,并忽略常数,因为当数量级足够大时,常数对于计算量的影响可以忽略不计;

如: (n-1)(n-2) 数量级为 n^2

时间复杂度使用大O表示,如O(1)

案例:

1.
void aFunction(){
    int c = 10 + 20;
    int d = c * c;
        printf(d);
}

上列算法若以赋值运算作为标准操作,则该算法的计算量为2,其时间复杂度记为O(1),为什么是O(1)呢,是因为2是一个常数,常数对于函数的增长影响并不大,所以计算量为常数时表示为O(1),按照这种方式,即使计算量为2000,同样记为O(1),称为常数

2.
void bFunction(int n){
  for(int i = 0;i < n;i++){ // n
    int c = 2 * i;// 1
    int d = 3 * i;// 2
  }
}

此时函数的循环次数由未知数n来决定,循环体内计算量为2,当n是一个自然数时,函数的计算量等于(n)(2),此时时间复杂度为O(n),无论用常数2对n进行加减乘除对于n的指数都没有影响,当n足够大时,内部的2次计算量可以忽略,所以记为O(n),称为线性阶

更粗陋的度量方法是函数体包含一层循环时记为O(n)

3.
void bFunction(int n){
  for(int i = 0;i < n;i++){
      for(int j = 0;j < i;j++){
      }
  }
}

外层循环次数为n,内层循环次数随着n的增长而增长且最大值为n-1次,那么整个函数的计算量为(n)(n-1),常数可以忽略,所以时间复杂度记为O(n^2) ,称为平方阶

粗陋的方法是有两层嵌套循环,且循环次数都随着n的增长而增长,所以是O(n^2),以此类推,但是要注意下面这种情况

4.
void bFunction(int n){
  for(int i = 0;i < n;i++){
      for(int j = 0;j < 3;j++){
      }
  }
}

此时内层循环的循环次数是固定(常数)所以不会影响计算量的数量级,时间复杂度记为O(n)

5.
void bFunction(int n){
  for(int i = 3;i < n;){
      i *= 3;
  }
}

此时函循环次数会随着3循环体中的常数3的的变化而变化,我们可以用对数来表示,

假设循环次数为s,循环条件可表示为 s = 3^s < n;(即i本身为3,其次幂会不断的增长,但结果要小于n)

当然这里有个条件是i的初值必须和每次乘等的值相同,形成次幂的增长;

用对数表示为s = log3n,时间复杂度记为O(log3n),常数可以忽略,所以最后是O(logn)称之为对数阶

对数阶的数量级小于线性阶,因为若n的值相同,对数阶计算量必然小于线性阶

6.
void bFunction(int n){
 for(int i = 0;i < n;i++){
     for(int j = 0;j < n;j++){
         for(int k = 0;k < n;k++){
       }
     }   
 }
}

上述算法时间复杂度为O(n^3),3为常数,对应着循环的嵌套层数;也可以用O(n^C)表示,称为多项式阶

7.
void bFunction(int n){
  int num = 2;
  for(int i = 0;i < n;){ //O(n)
        num *= 2;
  }
  for (int j = 0;j<num;j++){ //O(n)
  }
}

上述函数输入的参数n将作为循环次数的指数,假设循环次数为s, s = 2^n,那么时间复杂度为O(2^n),

称之为指数阶,可记为O(C^n)

8.
void bFunction(int n)
{
    for(int i=0;i<n;i++){
          for(int j=0;j<n;j++){
          }
    }   
  
      for(int i=0;i<n;i++){
      }             
}

对于顺序运行的算法,总时间复杂度等于算法中最大时间复杂度,即O(n^2)

9.
void bFunction(int n)
{
  if(n % 2 ==0){
    for(int i=0;i<n;i++){
          for(int j=0;j<n;j++){
          }
    }   
  }else{
      for(int i=0;i<n;i++){
      }             
  } 
}

对于具备分支结构的算法,总时间复杂度等于算法中时间复杂度最大路径的复杂度,即O(n^2)

时间复杂度大小顺序

常数 < 对数阶 < 线性阶 < 平方阶 < 多项式阶 < 指数阶

O(1) < O(logn) < O(n) < O(n^2) < O(n^C) < O(C^n)

优劣:

通常具有常数阶复杂度的算法是好的算法
具有指数阶复杂度的算法是差的算法

个人观点,若有错误敬请指出,谢谢!

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄