数据结构和算法

本文详细介绍了数据结构和算法的基础概念,包括数据结构的定义、算法的衡量标准时间复杂度和空间复杂度。通过实例解析了大O表示法,以及常见的时间复杂度分析方法如最好、最坏、平均和均摊时间复杂度。强调了数据结构和算法在软件开发中的核心地位,对于优化程序执行效率至关重要。

为什么要学习数据结构和算法?

万丈高楼平地起

前言

什么是数据结构和算法?

数据结构定义:

  以特定的数据类型和数据结构来存储数据,以及在此基础上实现某种功能(比如查找某个元素,删除某个元素,对元素进行排序等)而进行的操作和步骤,这个相应的操作也叫算法。

数据结构 = 个体数据的存储 + 个体数据的关系存储
算法 = 对存储数据的操作  
算法的定义:
  • 通俗的说算法是解题的方法和步骤
  • 衡量算法的标准
    • 时间复杂度:程序大概要执行的次数,而非执行的时间。
    • 空间复杂度:程序执行过程中大概所占用的最大内存空间。
    • 难易程度:容易懂,避免过于复杂。
    • 健壮性
数据结构的地位
  • 数据结构是软件中最核心的课程
  • 程序 = 数据的存储 + 数据的操作 + 可以被计算机执行的语言
预备知识
  1. 大O表示法
  2. 常见的时间复杂度分析方法
  3. 最好情况时间复杂度/最坏时间复杂度/平均情况时间复杂度/均摊实景复杂度
大O表示法:

公式:

  • T(n): 表示总的代码执行时间
  • f(n): 表示每行代码执行的次数总和
  • n: 表示数据规模的大小
    大 O 时间复杂度实际上并不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势。

例子1:

 int cal(int n) {
   int sum = 0;
   int i = 1;
   for (; i <= n; ++i) {//n 个unit_time
     sum = sum + i;//n 个unit_time
   }
   return sum;
 }

假设一行代码的执行时间为 1 个 unit_time ,则上述代码的总执行时间 T(n)=(1+1+n+n)∗unit_time=(2n+2)∗unit_timeT( n ) = (1+1+n +n) *unit\_time = (2n +2 )*unit\_timeT(n)=(1+1+n+n)unit_time=(2n+2)unit_time

例子2:

 int cal(int n) {
   int sum = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1;
     for (; j <= n; ++j) {
       sum = sum +  i * j;
     }
   }
 }

上述代码的执行时间: T(n)=(1+1+1+n+n+n∗n+n∗n)∗unit_time=(2n2+2n+3)∗unit_timeT( n ) = (1+1+1+n+n + n*n + n*n)*unit\_time = (2n^{2}+2n+3)*unit\_timeT(n)=(1+1+1+n+n+nn+nn)unit_time=(2n2+2n+3)unit_time

  由上述两个例子我们可以知道,所有代码的执行时间T(n) 和每行代码的执行次数f(n) 成正比,当我们的数据规模 n 越大时,比如1000、10000、100000时,公式中的低阶、常量、系数三部分对总数T(n)的影响越小,所以都可以忽略,我们只需要记录一个最大量级就可以了,则上述两个例子的大O表示法分别为:

  • 例子1:T(n)=(2n+2)∗unit_timeT( n ) = (2n +2 )*unit\_timeT(n)=(2n+2)unit_time
    它的大O 表示法为:T(n)=O(n) T(n) = O(n) T(n)=O(n)
  • 例子2:T(n)=(2n2+2n+3)∗unit_timeT( n ) = (2n^{2}+2n+3)*unit\_timeT(n)=(2n2+2n+3)unit_time
    它的大O表示法为:T(n)=O(n2)T(n) = O(n^{2}) T(n)=O(n2)
常用的时间复杂度分析方法

  因为大 O 表示法只是表示一种变化趋势。所以通常会忽略掉公式中的常量、低阶、系数,只需要记录一个最大阶的量级就可以了,下面是常用的三种计算准则

  1. 只关注循环执行次数最多的一段代码
    例如:
 int cal(int n) {
   int sum = 0;
   int i = 1;
   for (; i <= n; ++i) {
     sum = sum + i;
   }
   return sum;
 }

由于有一个for循环,所以时间复杂度为:T(n)=O(n) T(n) = O(n) T(n)=O(n)
2. 加法法则:总复杂度等于量级最大的那段代码的复杂度

int cal(int n) {
   int sum_1 = 0;
   int p = 1;
   for (; p < 100; ++p) {
     sum_1 = sum_1 + p;
   }

   int sum_2 = 0;
   int q = 1;
   for (; q < n; ++q) {
     sum_2 = sum_2 + q;
   }
 
   int sum_3 = 0;
   int i = 1;
   int j = 1;
   for (; i <= n; ++i) {
     j = 1; 
     for (; j <= n; ++j) {
       sum_3 = sum_3 +  i * j;
     }
   }
 
   return sum_1 + sum_2 + sum_3;
 }

  由于第一个for 循环的时间复杂度为 n,第二个for循环由于有循环的嵌套,所以时间复杂度为 n*n,总的时间复杂度为: T(n)=n2+n=O(n2)T( n )=n^{2}+n = O(n^{2})T(n)=n2+n=O(n2)

  1. 乘法法则:嵌套代码的复杂度等于嵌套内外代码复杂度的乘积
int cal(int n) {
   int ret = 0; 
   int i = 1;
   for (; i < n; ++i) {
     ret = ret + f(i);
   } 
 } 
 
 int f(int n) {
  int sum = 0;
  int i = 1;
  for (; i < n; ++i) {
    sum = sum + i;
  } 
  return sum;
 }

由于有循环的嵌套,所以时间复杂度为: T(n)=n∗n=O(n2)T( n ) =n*n = O(n^{2})T(n)=nn=O(n2)

由上我们可按如下规则来推到大O阶:

  1. 用常数1来取代运行时间中所有加法常数。
  2. 修改后的运行次数函数中,只保留最高阶项
  3. 如果最高阶项存在且不是1,则去除与这个项相乘的常数。
几种常见时间复杂度实例分析
  1. 常见的复杂度量级


  上述量级可粗略的分为两种:多项式量级非多项式量级。其中,非多项式量级只有两个:O(2n) 和 O(n!)。

  • O(1) :只是常量级时间复杂度的一种表示方法,并不是指只执行了一行代码
 int i = 8;
 int j = 6;
 int sum = i + j;

  只要代码的执行时间不随 n 的增大而增长,这样代码的时间复杂度我们都记作 O(1)。或者说,一般情况下,只要算法中不存在循环语句、递归语句,即使有成千上万行的代码,其时间复杂度也是Ο(1)。

  • O(logn)、O(nlogn)
 i=1;
 while (i <= n)  {
   i = i * 2;
 }

  从代码中可以看出,变量 i 的值从 1 开始取,每循环一次就乘以 2。当大于 n 时,循环结束,即对 2x=n 求解,x=log2n,所以,这段代码的时间复杂度就是 O(log2n)。
2. 算法执行效率与数据规模之间的增长关系图

我们可以粗略的人为 越高阶复杂度的算法,执行效率越低

最好、最坏、平均、均摊时间复杂度
  1. 最好、最坏时间复杂度
  • 最好情况时间复杂度就是,在最理想的情况下,执行这段代码的时间复杂度
  • 最坏情况时间复杂度就是,在最糟糕的情况下,执行这段代码的时间复杂度
// n表示数组array的长度
int find(int[] array, int n, int x) {
  int i = 0;
  int pos = -1;
  for (; i < n; ++i) {
    if (array[i] == x) pos = i;
  }
  return pos;
}

  上述代码为从一个数组中查找变量 x 出现的位置,其最好时间复杂度为 O(1),最坏时间复杂度为:O(n)

  1. 平均时间复杂度
      即各种情况下的概率的加权平均值,也叫加权平均时间复杂度或者**期望时间复杂度。**顾上述代码的平均时间复杂度为:

    用大 O 表示法来表示,去掉系数和常量,这段代码的加权平均时间复杂是 O(n)。通常我们使用一个复杂度就可以满足需求了,只有同一块代码在不同的情况下,时间复杂度有量级的差距,我们才会使用这三种复杂度表示法来区分。

  2. 均摊时间复杂度

 // array表示一个长度为n的数组
 // 代码中的array.length就等于n
 int[] array = new int[n];
 int count = 0;
 
 void insert(int val) {
    if (count == array.length) {
       int sum = 0;
       for (int i = 0; i < array.length; ++i) {
          sum = sum + array[i];
       }
       array[0] = sum;
       count = 1;
    }

    array[count] = val;
    ++count;
 }

 上述代码实现了一个往数组中插入数据的功能。当数组满了之后,也就是代码中的 count == array.length 时,我们用 for 循环遍历数组求和,并清空数组,将求和之后的 sum 值放到数组的第一个位置,然后再将新的数据插入。但如果数组一开始就有空闲空间,则直接将数据插入数组。

  它的最好时间复杂度为 O(1),最坏时间复杂度为 O(n),平均时间复杂度为

均摊时间复杂度就是一种特殊的平均时间复杂度,例子中每一次 O(n) 的插入操作,都会跟着 n-1 次 O(1) 的插入操作,所以把耗时多的那次操作均摊到接下来的 n-1 次耗时少的操作上,均摊下来,这一组连续的操作的均摊时间复杂度就是 O(1)。它可以简单的认为将最坏时间复杂的时间均摊到最高频次出现的时间复杂度上,所以均摊时间复杂度近似的等于最高频次出现的时间复杂度。

空间复杂度

表示算法的存储空间与数据规模之间的增长关系。

参考资料:

数据结构和算法之美
大话数据结构

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值