一、数据结构与算法绪论

一、数据结构的基本概念和术语

1、数据

数据是信息的载体,是描述客观事物属性的数、字符及所有能输入到计算机中并被计算机程序识别和处理的符号集合。数据是计算机程序加工的原料。

2、数据元素

数据元素是数据的基本单位,通常作为一个整体进行考虑和处理。一个数据元素可由若干数据项组成,数据项是构成数据元素的不可分割的最小单位。例如,学生记录就是一个数据元素,它是由学号、姓名、性别、年龄等数据项组成。

3、数据对象

数据对象是具有相同性质的数据元素的集合,是数据的一个子集。

4、数据类型

数据在计算机中存储必须按照数据的分类存储,根据分类应该用数据类型划分,数据类型用以刻画(程序)操作对象的特性,类型明显或隐含地规定了在程序执行期间变量或表达式所有可能的取值范围,以及在这些值上允许进行的操作。因此数据类型是一个值的集合和定义在这个值集合上的一组操作的总称。如程序语言中的整型变量、字符型变量,都确定了数据的取值范围。
分为三类:
1、原子类型:其值不可分解的数据类型,如高级语言中的基本数据类型(整型、字符型等)。
2、结构类型:是由若干成分按某种结构组成的,是可以分解的。
3、抽象数据类型(ADT):指抽象数据组织和与之相关的操作。每一个操作由它的输入和输出定义。抽象数据类型的定义取决于它的一组逻辑特性,而与其在计算机内的表示和实现无关,也就是说,无论其内部结构如何变化,只要其数学特性不变,都不影响其外部的使用。

5、数据结构

数据结构(Data Structure)是相互之间存在一种或多种特定关系的数据元素的集合。在任何问题中,数据元素都不是孤立存在的,它们之间存在某种关系,这种数据元素相互之间的关系称为结构(Structure)。数据结构包括三方面内容:逻辑结构、存储结构和数据的运算。
数据的逻辑结构和存储结构是密不可分的两个方面,一个算法的设计取决于所选定的逻辑结构,而算法的实现依赖于所采用的存储结构。

二、数据结构的三要素

1、数据的逻辑结构

逻辑结构是指数据元素之间的逻辑关系,即从逻辑关系上描述数据。它与数据的存储无关,是独立于计算机的。数据的逻辑结构分为线性结构和非线性结构
1、线性结构:在线性结构中,有且仅有一个开始和终端结点,并且所有节点都最多有一个直接前驱和一个直接后继。也就是,数据元素之间存在“一对一”的关系。常见的线性结构有:数组、链表、队列、栈等。
2、非线性结构:非线性结构就是表中各个结点之间具有多个对应关系(即一对多关系和多对多关系),在非线性结构的一个结点可能有多个直接前驱结点和多个直接后继结点。常见的非线性结构有:二维数组、多维数组、树结构、图结构等。

在这里插入图片描述

按照四类的基础逻辑结构划分:
1、集合:结构中的数据元素之间除“同属于一个集合外”,没有其他关系,如图a所示。
2、线性结构:结构中的数据元素之间只存在一对一的关系,如图b所示。
3、树型结构:结构中的数据元素之间存在一对多的层次关系,如图c所示。
4、图状结构或网状结构:结构中的数据元素之间存在多对多的任意关系,如图d所示。

在这里插入图片描述

2、数据的存储结构

存储结构是指数据结构在计算机中的表示(又称映像),也称物理结构。它包括数据元素的表示和关系的表示。数据的存储结构是用计算机语言实现的逻辑结构,它依赖于计算机语言。数据的存储结构主要有顺序存储、链式存储、索引存储和散列存储。
1、顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元里,元素之间的关系由存储单元的邻接关系来体现(数组采用的就是顺序存储结构)。其优点是可以实现随机存取,每个元素占用最少的的存储空间;缺点是只能使用相邻的一整块存储单元,因此可能产生较多的外部碎片。
2、链式存储:不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。其优点是不会出现碎片现象,能充分利用所有的存储单元;缺点是每个元素因存储指针而占用额外的存储空间,且只能实现顺序存取。
3、索引存储:在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。其优点是检索速度快;缺点是附加的索引表额外占用存储空间。另外,增加和删除数据时也要修改索引表,因而会花费较多的时间。
4、散列存储:根据元素的关键字直接计算出该元素的存储地址,又称哈希(Hash)存储。其优点是检索、增加和删除结点的操作都很快;缺点是若散列函数不好,则可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销。

3、数据的运算

施加在数据结构上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能;运算的实现是针对存储结构的,指出运算的具体操作步骤。

三、算法的基本概念

1、基本概念

算法(Algorithm)是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。
算法重要特性:
1、有穷性:算法必须在有限的步骤内结束。且每一步都在有限的时间内完成。如果一个算法由无限的步骤组成,该算法不可能有计算机程序实现。计算机只能解决有限问题,不能解决无限问题。
2、确定性:算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。
3、可行性:算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现。
4、输入:一个算法有零个或多个输入,这些输入取自于某个特定的对象的集合。
5、输出:一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。

2、算法设计要求

1、正确性:算法设计应满足具体问题的需求。它至少应该包括对输入、输出及加工过程等的明确的无歧义性的描述。
2、可读性:算法主要是为了阅读与交流,算法可读性好有助于对算法的理解。
3、健壮性:当输入非法的数据时,算法能够适当地作出反应或进行处理,不会产生莫名其妙的输出结果。
4、效率与低存储量需求:效率是指算法执行的时间,存储量需求是指算法执行过程中所需要的最大存储空间,这两者都与问题的规模有关。

3、数据结构与算法关系

两者既有联系又有区别,联系是程序=数据结构+算法。数据结构是算法实现的基础,算法总是要依赖某种数据结构来实现,本质上算法的操作对象就是数据结构。区别是数据结构关注的是数据的逻辑结构、存储结构相关的操作,而算法更多的是关注如何在数据结构的基础上解决实际问题。
所以说,算法是编程思想,数据结构则是这些思想的基础,高效的程序需要在数据结构的基础上设计和选择算法。

四、算法效率的度量

1、度量标准

算法效率的度量是通过时间复杂度和空间复杂度来描述的。

2、时间复杂度

语句的频度:是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为T(n),它是该算法问题规模n的函数,时间复杂度主要分析T(n)的数量级。算法中基本运算(最深层循环内的语句)的频度与T(n)同数量级,因此通常采用算法中基本运算的频度f(n)来分析算法的时间复杂度。
公式T(n)=O(f(n))
O:表示T(n)的数量级,其严格的数学定义表示,若T(n)f(n)是定义在正整数集合上的两个函数,则存在正常数C n 0 n_0 n0,使得当 n > = n 0 n>=n_0 n>=n0时,都满足0<=T(n)<=C*f(n)
1、分析算法时,存在几种可能的考虑:
  1. 算法完成工作最少需要多少基本操作,即最优时间复杂度
  2. 算法完成工作最多需要多少基本操作,即最坏时间复杂度
  3. 算法完成工作平均需要多少基本操作,即平均时间复杂度
  4. 对于最优时间复杂度,其价值不大,因为它没有提供什么有用信息,其反映的只是最乐观最理想的情况,没有参考价值。
  5. 对于最坏时间复杂度,提供了一种保证,表明算法在此种程度的基本操作中一定能完成工作,一般总是考虑在最坏情况下的时间复杂度
  6. 对于平均时间复杂度,是对算法的一个全面评价,因此它完整全面的反映了这个算法的性质。但另一方面,这种衡量并没有保证,不是每个计算都能在这个基本操作内完成。而且,对于平均情况的计算,也会因为应用算法的实例分布可能并不均匀而难以计算。
2、时间复杂度的几条基本计算规则:
  1. 基本操作,即只有常数项,认为其时间复杂度为 O(1) 。
  2. 顺序结构,时间复杂度按加法进行计算。
  3. 循环结构,时间复杂度按乘法进行计算。
  4. 分支结构,时间复杂度取最大值。
  5. 判断一个算法的效率时,往往只需要关注操作数量的最高次幂,可以忽略所有常数项、低次幂项和最高次幂的系数。
  6. 在没有特殊说明时,我们所分析的算法的时间复杂度都是指最坏时间复杂度。
@Test
public void test1(int n){
    int sum = 0;//执行1次
    for (int i = 0; i < n; i++) {//int i=0执行1次,i<n执行了n+1次,i++执行了n次
        sum += i;//执行了n次
    }
}
//语句频度:T(n)=3n+3
//时间复杂度:T(n)=O(n)

3、常见时间复杂度

1、常数阶O(1):无论代码执行了多少行,只要没有循环等复杂结构,那这个代码的时间复杂度就都是O(1)。在下面代码中,没有循环等复杂结构,它消耗的时间并不随着某个变量的增长而增长,那么无论代码多长,都可以用常数阶表示。
@Test
public void test2(){
    int num1 = 3, num2 = 5;
    int temp = num1;
    num1 = num2;
    num2 = temp;
    System.out.println("num1:" + num1 + " num2:" + num2);
}
2、对数阶:在循环中,每趟循环执行完毕后,循环变量都放大两倍。
推算过程:在下面代码中,假设该循环的执行次数为 X X X次(也就是 i i i的取值为 2 x 2^x 2x),就满足了循环的结束条件,即满足了 2 x = n 2^x=n 2x=n,通过数学公式转换后,即得到 x = l o g 2 n x=log2n x=log2n,也就是说最多循环 l o g 2 n log2n log2n次以后,这个代码就结束了,因此时间复杂度为 O ( l o g 2 n ) O(log2n) O(log2n)。如果循环变量放大3倍,那么时间复杂度为 O ( l o g 3 n ) O(log3n) O(log3n)
@Test
public void test3(){
    int n = 1024;
    for (int i = 0; i < n; i *= 2) {
        System.out.println(i);
    }
}
3、线性阶:在下面的代码中,for循环会执行n次,因此它消耗的时间随着n变化而变化的,因此这类代码都可以用 O ( n ) O(n) O(n)来表示时间复杂度。
@Test
public void test4(){
    int n = 1024;
    for (int i = 0; i < n; i ++) {
        System.out.println(i);
    }
}
4、线性对数阶:将对数阶的代码循环n次后,那么它的时间复杂度就是 n ∗ O ( l o g 2 n ) n*O(log2n) nO(log2n),也就是 O ( n l o g 2 n ) O(nlog2n) O(nlog2n)
@Test
public void test5(){
    int n = 1024;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j *= 2) {
            System.out.println(j);
        }
    }
}
5、平方阶:外层循环执行一次,内层循环就要执行n次,所以外层执行n次,那么总的需要执行n*n次,因此时间复杂度为 O ( n 2 ) O(n^2) O(n2)
@Test
public void test6(){
    int n = 10;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            System.out.println(j);
        }
    }
}
6、指数阶 O ( 2 n ) O(2^n) O(2n):当n为9的时候,需要执行 2 9 2^9 29次。
7、阶乘阶 O ( n ! ) O(n!) O(n!):当n为10的时候,需要执行 10 ∗ 9 ∗ 8 ∗ 7... ∗ 2 ∗ 1 10*9*8*7...*2*1 10987...21次。
执行次数函数举例非正式术语
12 12 12O( 1 1 1)常数阶
2 n + 3 2n+3 2n+3 O ( n ) O(n) O(n)线性阶
3 n 2 + 2 n + 1 3n^2+2n+1 3n2+2n+1 O ( n 2 ) O(n^2) O(n2)平方阶
5 l o g 2 n + 20 5log2^n+20 5log2n+20 O ( l o g n ) O(logn) O(logn)对数阶
2 n + 3 n l o g 2 n + 19 2n+3nlog2^n+19 2n+3nlog2n+19 O ( n l o g n ) O(nlogn) O(nlogn)线性对数阶
6 n 3 + 2 n 2 + 3 n + 4 6n^3+2n^2+3n+4 6n3+2n2+3n+4 O ( n 3 ) O(n^3) O(n3)立方阶
2 n 2^n 2n O ( 2 n ) O(2^n) O(2n)指数阶
注意:经常将 l o g 2 n log2^n log2n(以 2 为底的对数)简写成 l o g n logn logn
常见时间复杂度之间的关系:所消耗的时间从小到大
O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( n 3 ) < O ( 2 n ) < O ( n ! ) < O ( n n ) O(1)<O(logn)<O(n)<O(nlogn)<O(n^2)<O(n^3)<O(2^n)<O(n!)<O(n^n) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

4、空间复杂度

一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。程序执行时所需存储空间包括以下两部分:
  • 固定部分。这部分空间的大小与输入、输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
  • 可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。 这部分的空间大小与算法有关。
例如:要判断某年是不是闰年,你可能会花一点心思来写一个算法,每给一个年份,就可以通过这个算法计算得到是否闰年的结果。 另外一种方法是,事先建立一个有2050个元素的数组,然后把所有的年份按下标的数字应,如果是闰年,则此数组元素的值是1,如果不是元素的值则为0。这样,所谓的判断某一年是否为闰年就变成了查找这个数组某一个元素的值的问题。第一种方法相比起第二种来说很明显非常节省空间,但每一次查询都需要经过一系列的计算才能知道是否为闰年。第二种方法虽然需要在内存里存储2050个元素的数组,但是每次查询只需要一次索引判断即可。这就是通过一笔空间上的开销来换取计算时间开销的小技巧。到底哪一种方法好?其实还是要看你用在什么地方。
一个算法所需的存储空间用f(n)表示:S(n)=O(f(n))其中n为问题的规模,S(n)表示空间复杂度。
通常,我们都是用“时间复杂度”来指运行时间的需求,是用“空间复杂度”指空间需求。当直接要让我们求“复杂度”时,通常指的是时间复杂度。显然对时间复杂度的追求更是属于算法的潮流!
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值