1.绪论 1.1.1 数据结构的定义 数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。 数据元素:数据元素是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。也被称为记录。 数据对象:是性质相同的数据元素的集合,是数据的子集。 数据结构:是互相之间存在一种或多种特定关系的数据元素的集合。 1.1.2 数据的逻辑结构 逻辑结构:是指数据对象中数据元素之间的相互关系。包括: 1、集合结构:集合结构中的数据元素除了同属于一个集合外,他们之间没有其他关系 2、线性结构:线性结构中数据元素之间是一对一的关系 3、树形结构:树形结构中的数据元素之间存在一种一对多的层次关系 4、图形结构:图形结构的数据元素是多对多的关系 1.1.3 数据的存储结构 物理结构:是指数据的逻辑结构在计算机中的存储形式。 1、顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的 2、链式存储结构:是把数据元素存放在任意的存储单元里,这组储存单元可以是连续的,也可以是不连续的。 1.1.4 数据的运算 施加在数据上的运算包括运算的定义和实现。运算的定义是针对逻辑结构的,指出运算的功能,运算的实现是针对存储结构的,指出运算的具体操作步骤。 1.1.5 数据结构和数据类型 数据类型 数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。在C语言中,按照取值的不同,数据类型可以分为两类: 1、原子类型:是不可以再分解的基本类型,包括整形、实型、字符型等 2、结构类型:由若干个类型组合而成,是可以再分解的。例如整形数组是由若干整形数据组成。 抽象数据类型 抽象:是指抽取出事物具有的普遍性的本质。 抽象数据类型:是指一个数学模型及定义在该模型上的一组操作。 事实上,抽象数据类型体现了程序设计中问题分解、抽象和信息隐藏的特性。 1.2 算法及其描述 1.2.1 算法的基本概念 算法是对特定问题求解步骤的一种描述,它是指令的有限序列,其中的每条指令表示一个或多个操作。因此,算法具有以下五个重要的特性: (1)有穷性。一个算法必总在执行又穷步之后结束,且每一步都可在有穷时间内完成。 (2)确定性。算法中每条指令必须有确切的含义,对于相同的输入只能得出相同的输出。 (3)可行性。算法中描述的操作都可以通过已经是实现的基本运算执行有限次来实现。 (4)输入。一个算法有零个或多个输入,这些输入曲子某个特定的对象的集合。 (5)输出。一个算法有一个或多个输出,这些输出是与输入有着某种特定关系的量。 1.3 算法分析 时间复杂度 一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有的语句的频度之和记为T(n),它是该算法问题规模n的函数,时间复杂度主要分析T(n)的数量级。算法中基本运算(最深层循环内的语句)的频度与T(n)同数量级,因此通常采用算法中基本运算的频度f(n)来分析算法的时间复杂度。因此,算法的时间复杂度记为: T(n)=O(f(n)) O的含义是T(n)的数量级,其严格的数学定义是:若T(n)和f(n)是定义在正整数集合上的两个函数,则存在正常数C和n0,使得当n>=n0时,都满足0<=T(n)<=Cf(n). 算法的时间复杂度不仅依赖于问题的规模n,也取决于待输入数据的性质(如输入数据元素的初始状态)。 最坏时间复杂度是指在最坏情况下,算法的时间复杂度。 平均时间复杂度是指所有可能输入实例在等概率出现的情况下,算法的期望运行时间。 最好时间复杂度是指在最好的情况下,算法的时间复杂度。 一般总是考虑在最坏情况下的时间复杂度,以保证算法的运行时间不会比它更长。 在分析一个程序的时间复杂性时,有以下两条规则: (1)加法规则 T(n)=T1(n)+T2(n)=O(f(n))+O(g(n))=O(max(f(n),g(n))) (2)乘法规则 T(n)=T1(n)*T2(n)=O(f(n))*O(g(n))=O(f(n)*g(n)) 常见的渐进时间复杂度为: O(1)<O(log2n)<O(nlog2n)<O(n^2)<O(n ^3)<O(2 ^n)<O(n!)<O(n ^n) 空间复杂度 算法的空间复杂度S(n)定义为算法所耗费的存储空间,它是问题规模的函数。记为: S(n)=O(g(n)) 一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间只取决于问题本身,和算法无关,则只需分析除输入和程序之外的额外空间。 算法原地工作是指算法所需的辅助空间为常亮,即O(1). 2.线性表 (一)初始化 顺序表:为顺序表分配一个大小确定的数组空间,空表时长度为0; 链表:构造一个空的单链表L,用头指针指向头结点,头结点的指针域置空(L = new LNode; L->next = NULL;) (二)取值 顺序表:先判断序号值是否合理,若合理,则e = L.elem[ i - 1 ] ; 链表:用指针p指向首元结点,用j做计数器初值赋为1,从首元结点开始依次顺着链域next向下访问 p=L->next; j=1; while(p&&j<i) { p=p->next; ++j; } e = p->data; (三)查找(按值查找) 顺序表:从第一个元素起,依次与待查找数比较,时间复杂度为O(n) for( int i=0; i<n; i++ ) if( L.elem[i]==e) return i+1; 链表:用指针p指向首元结点,从首元结点开始依次顺着链域next向下访问,时间复杂度为O(n) p=L->next; while( p && p->data != e ) p=p->next; (四)插入(在第i个位置插入新的元素e) 顺序表:将第n个至第i个位置依次向后移动一个位置,空出第i个位置,将元素e放入第i个位置,表长加1,时间复杂度为O(n) for( int j=L.length - 1; j>=i -1; j-- ) L.elem[ j+1]=L.elem[ j ]; L.elem[ i -1 ]=e; ++L.length; 链表:先查找到第i-1个结点,再将值为e的新结点插入到结点a(i-1)和a(i)之间,时间复杂度为O(n) p = L; j = 0; while( p && j<i-1 ) { p=p->next; ++j; } s = new LNode; s->data=e; s->next = p->next; p->next = s; (五)删除 顺序表:先判断位置i是否合理,若合理,则将被删除元素之后的元素前移,表长减1,时间复杂度为O(n) 链表:先查找到第i-1个结点,临时保存被删除结点的地址以备释放,改变删除结点前驱结点的指针域,释放删除结点的指针域,时间复杂度为O(n) 如何选取存储结构 1、基于存储的考虑 难以估计线性表的长度或存储规模时,不宜采用顺序表:链表不用事先估计存储规模,但链表的存储密度较低,显然链式存储结构的存储密度是小于1的。 2、基于运算的考虑 在顺序表中按序号访问a,的时间复杂度为O(1),而链表中按序号访问的时间复杂度为O(n),因此若经常做的运算是按序号访问数据元素,则显然顺序表优于链表。在顺序表中进行插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应忽视的;在链表中进行插入、删除操作时,虽然也要找插入位置,但操作主要是比较操作,从这个角度考虑显然后者优于前者。 3、基于环境的考虑 顺序表容易实现,任何高级语言中都有数组类型:链表的操作是基于指针的,相对来讲,前者实现较为简单,这也是用户考虑的一个因素。总之,两种存储结构各有长短,选择哪一种由实际问题的主要因素决定。通常较稳定的线性表选择顺序存储,而频繁进行插入、删除操作的线性表(即动态性较强)宜选择链式存储。