一、绪论
1.1数据结构的三要素
逻辑结构:是指数据元素之间的逻辑关系,与数据的存储无关是独立于计算机的
- 集合
- 线性
- 树形
- 图状或网状
存储结构:是指数据结构在计算机中的表示,包括数据结构元素的表示和关系的表示
- 顺序存储:把逻辑上相邻的元素存储在物理位置也相邻的存储单元中,元素之间的关系由存储单元邻接关系来体现。优点:可实现随机存取,每个元素占最少的存储空间,缺点:只能使用相邻的一整块存储单元,可能产生较多的外部碎片。
- 链式存储:不要求逻辑上相邻的元素在物理上也相邻借助指示元素地址的指针来表示元素之间的逻辑关系。优点:不会出现碎片化现象,能充分利用所有存储单元,缺点每个元素因存储指针而占用额外的存储空间,且只能顺序存取。
- 索引存储:在存储元素信息的同时,还建立附加的索引表。索引表中的每个项称之为索引项,其一般形式(关键字,地址)。优点:检索速度快,缺点:索引表占用额外空间,增删元素时需要修改索引表花费时间。
- 散列存储:根据元素的关键字直接计算出元素的存储地址,又称哈希存储(hash)。优点:检索、增删元素操作快,缺点:若hash函数不好可能出现元素存储单元的冲突,解决这些冲突会花费时间和空间
数据元素运算:运算的定义针对逻辑结构,运算的实现针对存储结构。
1.2数据结构是一个研究什么的科学
答:
数据结构是一门研究非数值计算的程序设计中计算机的操作对象以及它们之间的关系和操作的学科
1.3数据类型和抽象数据类型是如何定义的,有何相同与不同之处?抽象数据类型的主要特点是什么?使用抽象数据类型的好处是什么?
答:数据类型:是程序设计语言中的一个概念,它是一个值的集合和操作的集合,如c语言中整型、字符型、操作有加减乘除。
抽象数据类型(ADT):指一个数学模型及定义在该模型上的一组操作,抽象数据类型的定义只却决于它的逻辑特征,抽象数据类型和数据类型实质上是一个概念。此外抽象数据类型的范围更广,使用抽象数据类型定义的软件模块含定义、表示和实现三部分,封装在一起对用户透明而不必了解实现细节。
1.4算法
答:算法:是对特定问题求解步骤的一种描述,它是指令的有限序列,其中每条指令表示一个或多个操作。算法的五个重要特性:有穷性、确定性、可行性、输入、输出,设计一个好的算法包括:正确性、可读性、健壮性、好的时间和空间复杂的。
算法的五个重要特征:有穷性、确定性、可行性、输入、输出
好的算法应该达到:正确性、可读性、健壮性和效率与低存储量
1.4.1时间复杂度
一个语句的频度是指该语句在算法中被重复执行的次数。算法中所有语句的频度之和记为T(n), 它 是该算法问题规模n 的函数,时间复杂度主要分析T(n) 的数量级。算法中基本运算(最深层循环 内的语句)的频度与T(n) 同数量级,因此通常采用算法中基本运算的频度f(n)来分析算法的时间复 杂度。
因此,算法的时间复杂度记为 T(n) = O(f(n)) 取f(n) 中随n 增长最快的项,将其系数置为1 作为时间复杂度的度量。例如, f(n) = an3 + bn2 + cn 的时向复杂度为O(n3 ) 上式中, O 的含义是T(n) 的数量级,其严格的数学定义是:若T(n)和f(n)是定义在正整数集合上的 两个函数,则存在正常数C 和n0,使得当n >= n0时,都满足0 <=T(n) <=Cf(n) 。
算法的时间复杂度不仅依赖于问题的规模n, 也取决于待输入数据的性质(如输入数据元素 的初始状态)
1.4.2空间复杂度
算法的空间复杂度S(n)定义为该算法所耗费的存储空间,它是问题规模n 的函数。记为 S(n) = O(g(n)) 一个程序在执行时除需要存储空间来存放本身所用的指令、常数、变量和输入数据外,还需要一 些对数据进行操作的工作单元和存储一些为实现计算所需信息的辅助空间。若输入数据所占空间 只取决于问题本身,和算法无关,则只需分析除输入和程序之外的额外空间。 算法原地工作是指算法所需的辅助空间为常量,即O(1) 。
1.4.3递归的效率比循环的效率高吗?
循环和递归两者是可以互换的,不能决定性的说循环的效率比递归高。
递归的优点是:代码简洁清晰,容易检查正确性;缺点是:当递归调用的次数较多时,要增加额 外的堆栈处理,有可能产生堆栈溢出的情况,对执行效率有一定的影响。
循环的优点是:结构简单,速度快;缺点是:它并不能解决全部问题,有的问题适合于用递归来 解决不适合用循环。
1.4.4贪心算法和动态规划以及分治法的区别?
贪心算法顾名思义就是做出在当前看来是最好的结果,它不从整体上加以考虑,也就是局部最优 解。贪心算法从上往下,从顶部一步一步最优,得到最后的结果,它不能保证全局最优解,与贪 心策略的选择有关。
动态规划是把问题分解成子问题,这些子问题可能有重复,可以记录下前面子问题的结果防止重 复计算。动态规划解决子问题,前一个子问题的解对后一个子问题产生一定的影响。在求解子问 题的过程中保留哪些有可能得到最优的局部解,丢弃其他局部解,直到解决最后一个问题时也就 是初始问题的解。动态规划是从下到上,一步一步找到全局最优解。(各子问题重叠)
分治法(divide-and-conquer):将原问题划分成n个规模较小而结构与原问题相似的子问题; 递归地解决这些子问题,然后再合并其结果,就得到原问题的解。(各子问题独立) 分治模式在每一层递归上都有三个步骤: 分解(Divide):将原问题分解成一系列子问题; 解决(conquer):递归地解各个子问题。若子问题足够小,则直接求解; 合并(Combine):将子问题的结果合并成原问题的解。 例如归并排序。
二、线性表
2.1定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列。
2.2线性表的顺序表示
线性表的顺序存储又称顺序表,特点逻辑顺序与物理顺序相同,存储密度高每个结点只存元素能实现随机存取,但插入和删除需要一定大量元素。
线性表的顺序存储类型描述:
#define MAXSIZE 100
typedef struct
{
Elemtype* data;//动态分配顺序表指针
int length;//当前元素个数
int MAXSIZE;最大容量
}Seqlist //顺序表的数据类型
2.3顺序表的主要操作
插入:所需移动结点的平均次数为:n/2;平均时间复杂度O(n)
删除:所需移动结点的平均次数为:(n-1)/2;平均时间复杂度O(n)
按值查找:平均比较次数:(n+1)/2; 平均时间复杂度O(n)
2.4线性表的链式表示
- 单链表:通过一组任意存储单元来存储线性表中的数据元素,元素之间的关系通过指针来实现。特点非随机存取,但插入和删除比较方便。
typedef struct Lnode
{
Elemtype data;
struct Lnode* next;
}Lnode,*LinkList;
- 引入头结点后的两个优点:
- 由于第一个数据结点的位置被存储在头结点的指针域中,因此在链表的第一个位置的操作和在表中其他位置的操作一致。无需特殊处理。
- 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也统一了。
2.5单双链表的插入操作
2.6 顺序表和链表的比较
1.存取(读写)方式 顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。例如在第i个位置上执行 存或取的操作,顺序表仅需一次访问,而链表则需从表头开始依次访问i次。
2.逻辑结构与物理结构 采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上 相邻的元素,物理存储位置则不一定相邻,对应的逻辑关系是通过指针链接来表示的。
3.查找、插入和删除操作 对于按值查找,顺序表无序时,两者的时间复杂度均为O(n); 顺序表有序时,可采用折半查找,此 时的时间复杂度为O(log2n) 。 对于按序号查找,顺序表支持随机访问,时间复杂度仅为0(1), 而链表的平均时间复杂度为O(n) 。 顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需修改相 关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大。
4.空间分配 顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,若再加入新元素,则会出现内 存溢出,因此需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲 置;预先分配过小,又会造成溢出。动态存储分配虽然存储空间可以扩充,但需要移动大量元 素,导致操作效率降低,而且若内存中没有更大块的连续存储空间,则会导致分配失败。链式存 储的结点空间只在需要时申请分配,只要内存有空间就可以分配,操作灵活、高效。
三、栈和队列
3.1栈
1)栈的定义
栈操作受限的线性表,限制在表的一端进行插入和删除,性质后进先出(LIFO)或先进后出,栈顶(Top)允许进行插入、删除操作的一端,又称表尾,用栈顶指针Top来指示栈定元素,栈底(Bottom)是固定端,又称表头。
2)进栈和出栈
n个不同元素进栈出栈元素不同排列个数为(卡特兰数)1/(n+1)*Cn2n,C48=8*7*6*5/4*3*2*1
3)栈的操作
栈的动态存储的临界条件:栈空、栈满、Top指向、Bottom指向、栈元素个数,取决于栈的初始化:假如开辟了【0---n-1】共n个空间存储元素;
①初始化时,Top=Bottom=-1;
②栈空:Top==Bottom=-1;栈满:Top==n-1||(T-b)>=n时;Bottom=-1;
③元素进栈:Top指向栈顶元素Top++;Stact.arr[top]=e;出栈:pop();Top--;
栈中元素个数:Top-Bottm;
④初始化时,Top=Bottm=0;栈空:Top==Bottom=0;B始终指向0;
⑥进栈:push();T++;出栈:pop();T--;T指向栈顶元素的下一个位置;
⑦栈满:T-B>=n时||T==n-1;
⑧初始化时,T=B=n;栈空T=B=n;B=n;
⑨进栈:T--;push();Top指向栈顶元素;出栈:pop();Top++;
⑩栈满:|t-b|>=n;栈中元素个数:|T-B| 个
初始化时,T=B=n-1;同理;
4)共享栈(对顶栈)
把两个栈的栈底设定在数组两端|top1-top2|==1;
5)栈的应用:括号匹配、进制转换、函数和递归、表达式求值
①括号匹配
例如[4+(2+8)*[5/(9-7)]*3]
只看括号匹配对否:
匹配思想:从左至右扫描一个字符串或是表达式,则每个右括号将与最近遇到的那个左括号相匹配。从左至右扫描的过程中把所遇到的左括号放进堆栈中,每当遇到一个右括号时就将它与栈顶的左括号(如果存在)相匹配,同时从栈顶删除该左括号。
思想:
设置一个栈;
当读到左括号时,左括号进栈
当读到右括号时,从栈中弹出一个元素,与读到的右括号进行匹配,若成功继续读入
否则匹配失败返回False
//括号匹配
int Match_Brackets()
{
char ch, x;
std::stack<char> S;
scanf("%c",&ch);
while (ch!=13)//程序到回车结束
{
if ((ch == '(') || (ch == '[')) S.push(ch);
else if(ch==']')
{
x = S.top();
S.pop();
if (x != '[')
{
std::cout << "匹配失败";
return false;
}
}
else if (ch == ')')
{
x = S.top();
S.pop();
if (x != '(')
{
std::cout << "匹配失败";
return false;
}
}
}//while
if (!S.empty())
{
std::cout << "括号数量不匹配";
return false;
}
else return true;
}
②进制转换
//进制转换
int conversation(int n,int d)
{
std::stack<int> S;
int k, e;
while (n>0)
{
k = n % d;
S.push(k);
n = n / d;
}
while (!S.empty())
{
std::cout << S.top();
S.pop();
}
}
③表达式求值
前缀、中缀、后缀表达式
以操作数为叶子结点运算符为根结点(先运算的先构造)构造成为一颗二叉树
如:b-(a+5)*3
线序遍历=前缀,中序=中缀,后续=后缀
表达式求值:
设置两个栈:S1数栈,S2符栈--- ①从左往右依次扫描,遇数字直接S1.push(e);
②若遇到符号,设符号位ch:
若S2为空则S2.push(ch)
若ch为左括号则S2.push(ch)
若S2的栈顶为左括号则S2.push(ch)若ch优先级大于S2栈顶元素的优先级则S2.push(ch),否则将从S2中弹出栈顶元素并且从S1中弹出两个数运算然后将运算结果压回S1中,然后回至第二步继续执行
④函数和递归
在函数和递归中
函数调用的特点:最后被调用的函数最先执行结束(LIFO)
函数调用时,需要用一个栈存储:
①调用返回地址
②实参
③局部变量
3.2队列
1)队列的定义
受限的线性表,只允许在一端进行删除另一端进行插入,先进先出队首(Front):进行删除的一端;队尾Rear:进行插入的一端
2)队列假溢现象产生的原因
在非空队列里,队首指针始终指向队头元素,队尾指针始终指向队尾元素的下一个位置当进行一系列入队和出队操作后,可能出现尽管队列中实际元素个数可能远远小于数组大小,但由于队尾指针已经向量空间的上限`故而不能进行入队操作的现象,这种现象称之为假溢。
克服上述假溢出现象的方法是:将为队列分配的向量空间看成为一个首位相接的圆环,并称这种队列为循环队列:
循环队列为空:front=rear;
循环队列满:(rear+1)%Maxsize=front;//约定:牺牲一个位置不存储,非空情况下,rear指向队尾元素的下一个空位置,front指向队首元素
入队:rear=(rear+1)%Maxsize;先入元素,rear++
出队:front=(front+1)%Maxszie;先出元素,front++
循环队列中元素个数:Len=(rear-front+Maxszie)%Maxsize
3)队列的应用:树的层次遍历、数据的缓冲区、进程调度的任务队列
3.4栈和队列的区别?
队列是允许在一段进行插入另一端进行删除的线性表。队列顾名思义就像排队一样,对于进入队 列的元素按“先进先出”的规则处理,在表头进行删除在表尾进行插入。由于队列要进行频繁的插 入和删除,一般为了高效,选择用定长数组来存储队列元素,在对队列进行操作之前要判断队列 是否为空或是否已满。如果想要动态长度也可以用链表来存储队列,这时要记住队头和对位指针 的地址。 栈是只能在表尾进行插入和删除操作的线性表。对于插入到栈的元素按“后进先出”的规则处理, 插入和删除操作都在栈顶进行,与队列类似一般用定长数组存储栈元素。由于进栈和出栈都是在 栈顶进行,因此要有一个size变量来记录当前栈的大小,当进栈时size不能超过数组长度, size+1,出栈时栈不为空,size-1。