数据结构-1 基本概念

140 阅读4分钟

数据结构与算法

数据结构

数据对象在计算机中的组织方式,包括逻辑结构(如线性结构、树、图等)和物理存储结构

算法

是一个有限的指令集,它可以接受一些输入(有时不需要),产生一些输出,会在有限的步骤之后终止。

算法的指令需要满足的条件

  1. 明确的目标,不可有歧义
  2. 在计算机能处理的范围内

一般对算法中指令的描述不依赖于具体的编程语言和实现方式(抽象)。

数据结构和算法的关系

基于特定的目的,数据对象往往会被施加操作,而完成这些操作的方法就是算法

抽象数据类型

数据类型:包括数据对象集及其相关联的操作集

抽象:描述数据类型的方法不依赖于具体实现,与存储机器、物理结构、实现算法、编程语言等无关。

抽象数据类型只关心是什么,不关心如何做到,以下图为例:

抽象数据类型.png

算法的效率

算法效率的影响因素

  1. 数据量
  2. 数据组织方式(eg. 摆书的方式)
  3. 空间利用方式(eg. 循环 vs 递归)
  4. 算法巧妙程度

算法效率的衡量标准

  1. 空间复杂度 S(n)S(n) :输入数据量为n时,程序执行占用存储单元的长度
  2. 时间复杂度 T(n)T(n) :输入数据量为n时,程序执行耗费时间的长度。

T(n)T(n) 为例,衡量算法效率时一般考虑最坏情况复杂度 Tworst(n)T_{worst}(n) 和平均复杂度 Tavg(n)T_{avg}(n) ,一般以 O(f(n))O(f(n)) 表示(渐进表示法)。

O(f(n))O(f(n)) 定义:存在常数 C>0C>0n0>0n_0>0 ,使得当 nn0n\geq n_0 时有 T(n)Cf(n)T(n)\leq C\cdot f(n)

分析复杂度的若干技巧

  1. 若两段算法分别具有时间复杂度 T1(n)=O(f1(n))T_1(n)=O(f_1(n))T2(n)=O(f2(n))T_2(n)=O(f_2(n)) ,则它们拼接嵌套后时间复杂度 T(n)T(n) 分别如下:
拼接:T(n)=T1(n)+T2(n)=max(O(f1(n),O(f2(n)))拼接:T(n)=T_1(n)+T_2(n)=max(O(f_1(n), O(f_2(n)))
嵌套:T(n)=T1(n)×T2(n)=O(f1(n)×f2(n))嵌套:T(n)=T_1(n)\times T_2(n)=O(f_1(n)\times f_2(n))
  1. for循环的时间复杂度 == 循环次数 ×\times 循环体的复杂度
  2. if-else结构的时间复杂度 == max(max( if条件判断部分的复杂度 ,, 各分支部分的复杂度 ))

程序执行耗时的实测方法

#include <stdio.h>
#include <time.h> /* 包含计时所需的函数 */

/* clock()函数可以返回程序开始运行到该函数被调用时所耗费的时间,
 * 单位为clock tick */
clock_t start, stop; /* clock_t为clock()函数返回的变量类型 */
double duration; /* duration记录被测函数运行时间,以秒为单位 */

int main() {
  /* 此处写测试范围之前的步骤 */

  start = clock(); /* 记录开始时刻(clock tick) */
  MyFunction();    /* 被测函数 */
  stop = clock();  /* 记录停止时刻(clock tick) */
  duration = ((double)(stop - start)) / CLK_TCK;
  /* 将被测函数运行经过的clock tick转化为秒 */
  /* 常数CLK_TCK表示每秒钟走过的clock tick数 */

  /* 此处写测试范围之后的步骤 */

  return 0;
}

如要比较两个耗时很短的被测函数,需要通过反复调用被测函数循环)来取得充分的打点数(最后将总时间除以调用的次数),否则可能因为打点数过小,两者运行时间不准确(eg. 两个函数运行一次均不足一个clock tick,则运行时间均被记录为0)。

本节要求掌握的算法

最大子列和问题

在线处理

int MaxSubseqSum1(int List[], int N) {
  int i;
  int thisSum = 0, maxSum = 0;

  for (i = 0; i < N; i++) {
    thisSum += List[i]; /* thisSum向右累加 */
    if (thisSum > maxSum)
      maxSum = thisSum; /* 发现更大的子列和就更新maxSum */
    else if (thisSum < 0)
      thisSum = 0; /* 如果thisSum已经小于0,则抛弃前面的部分重新开始累加 */
  }

  return maxSum;
}

分治法

/* 返回三个数中的最大值 */
int Max3(int a, int b, int c) { return a > b ? a > c ? a : c : b > c ? b : c; }

/* 分治法求最大子列和 */
int DivideAndConquer(int List[], int left, int right) {
  /* 拆分成三个小问题:左右的以及横跨两边的最小子列和,最后返回三者中最大的 */
  int maxLeftSum = 0, maxRightSum = 0;
  int maxLeftBorderSum = 0, maxRightBorderSum = 0;
  int center = (left + right) / 2;
  int i;

  /* 递归终止条件 */
  if (left == right) {
    if (List[left] < 0)
      return 0;
    else
      return List[left];
  }

  /* 左右两边的最小子列和 */
  maxLeftSum = DivideAndConquer(List, left, center);
  maxRightSum = DivideAndConquer(List, center + 1, right);

  /* 横跨两边的最小子列和 */
  int thisLeftBorderSum = 0, thisRightBorderSum = 0;
  /* 扫描左半边 */
  for (i = center; i >= left; i--) {
    thisLeftBorderSum += List[i];
    if (thisLeftBorderSum > maxLeftBorderSum)
      maxLeftBorderSum = thisLeftBorderSum;
  }
  /* 扫描右半边 */
  for (i = center + 1; i <= right; i++) {
    thisRightBorderSum += List[i];
    if (thisRightBorderSum > maxRightBorderSum)
      maxRightBorderSum = thisRightBorderSum;
  }

  /* 返回三种情况中最大的子列和 */
  return Max3(maxLeftSum, maxRightSum, maxLeftBorderSum + maxRightBorderSum);
}

/* 将函数接口与上一种方法保持一致 */
int MaxSubseqSum2(int List[], int N) {
  return DivideAndConquer(List, 0, N - 1);
}

二分查找

int BinarySearch(int List[], int N, int X) { /* N为列表长度,X为待查找的值 */
  int left = 0, right = N - 1;
  int mid = left + ((right - left) / 2);
  /* 等同于 (left + right) / 2,防止溢出 */

  while (left <= right) {
    if (X == List[mid])
      return mid; /* 如果找到,返回X在列表中的位置 */
    else if (X > List[mid])
      left = mid + 1;
    else
      right = mid - 1;
    mid = left + ((right - left) / 2);
  }

  /* 如果没找到返回-1 */
  return -1;
}

多项式求和

double PolynominalSum(double a[], int n, double x) {
  /* a[]按指数顺序存放系数,从0开始,n表示最大的指数,x表示底数 */
  double p = 0;
  int i;

  for (i = n; i >= 0; i--) p = p * x + a[i];

  return p;
}