15 第十五章 数据结构.docx
《15 第十五章 数据结构.docx》由会员分享,可在线阅读,更多相关《15 第十五章 数据结构.docx(49页珍藏版)》请在冰豆网上搜索。
15第十五章数据结构
第十五章:
数据结构
学习目标
⏹数组的结构以及各种操作
⏹单链表、双链表、循环链表的实现及操作
⏹栈、队列的实现以及各种基本操作
⏹二叉树的结构与实现
⏹遍历二叉树
数据结构介绍:
软件(software)是计算机系统中与硬件(hardware)相互依存的一部分,它包括程序(program)、相关数据(data)及其说明文档(document)。
程序来操作数据,如果数据组织的不好就会造成效率问题,甚至造成程序不能运行。
因此数据结构就诞生了。
数据结构从概念上讲有数据的逻辑结构和数据的物理结构(也叫存储结构)。
数据的逻辑结构就是数据表面上的关系。
例如:
如果每个数据节点,除了首节点和尾节点外,每个节点都只有一个直接前驱和一个直接后继,那么就是个线性表。
该类型常见的结构有:
数组,队列,堆栈,链表等。
如果除了首节点外,每个节点都只有一个直接前驱和多个直接后继,那么该结构就是一个树型结构。
如果每个节点都有多个直接前驱和多个直接后继,那么就是图形结构。
数据的物理结构只有两个:
一个是顺序存储,一个是链接式存储。
下面将使用JAVA语言来实现各个数据结构以及算法。
数组
通过索引(下标)来访问数据元素。
索引是一种随机访问操作。
随机访问的特点就是不需要从数组的第一个元素开始访问要查找的元素,而是可以直接访问查找的元素,在随机访问中,查找第一个元素和查找最后一个元素所需时间是一样的。
数组中的数据是保存在一个地址连续的内存中,它们是紧密相邻的。
当查找第i个元素时,计算机是将数组的基地址加上数据的偏移量的值来确定第i个元素的。
基地址指第一个元素的地址,偏移量等于第i个元素的索引乘以一个常量,这个常量是数组中一个元素所需的内存单元数目(字节)。
在java和c++中,所有的数组都是动态数组,即数组的大小可以在运行的时候才确定。
比如:
voidm1(inti){
int[]arra=newint[i];
}
逻辑大小和物理大小
物理大小指数组的容量大小,而逻辑大小则指数组中实际已存储的数据元素个数。
Java中通过数组的length属性来得到数组大小
如果数组的逻辑大小等于物理大小,则代表数组已满;如果数组的逻辑大小为0,则代表数组为空;在数组已满的情况下,数组的最后一个元素的索引(下标)值为逻辑大小减一。
链表
由多个节点(对象)组成,其中每个节点在内存中是散乱存放的,即存放在一个叫做对象堆的不连续地址内存中。
其中每个节点(除了尾节点外)都有一个特定的引用下一个节点的变量,从而实现一个完整链表。
链表中的每个元素都称为节点,每个节点包含数据内容和引用后一个节点的变量(通常叫做指针)。
数组中的元素是存储在地址连续的内存中,而链表中的元素则是散乱存放在内存中的。
不能通过索引来访问链表,查找链表中的数据则必须从链表的一端开始沿着链一直移动直到找到查询的数据为止。
由于链表的不连续地址内存机制,在往链表插入和删除数据时,不会象数组那样移动其他数据项,而是在空闲内存中为新的节点对象分配空间,然后将该节点和链表中特定位置的节点链接起来。
链表类型
单链表
单链表节点包含一个数据内容和指向后一个节点的引用。
其中,单链表的最后一个节点的指针指向null,第一个节点用一个特殊的头节点来指向。
单链表只能以向后的方式访问链表节点!
单链表的实现及各种操作:
见例题。
双向链表
双向链表节点不但包含单链表节点有的内容之外,还包含一个指向前一个节点的引用,通过向前指针和向后指针来实现双向访问链表节点!
同样,双向链表也有头部和尾部,有两个特殊链表指针,分别指向第一个节点和最后一个节点。
双向链表和单链表的区别在于,双向链表可以前后访问节点!
双向链表的实现和操作:
见例题。
循环链表
最后一个节点的指针指向第一个节点,其余和单链表类似。
实现和操作:
见例题。
三、栈(stack)
一个仅在一端访问的线性集合,这端叫做top(栈顶)。
遵循后进先出的协议(LIFO),即最后加入栈的数据会最先被取出。
push----下推数据到栈
pop----从栈顶取出最后一个被推进来的数据
栈的应用:
⏹中缀表达式到后缀表达式的转换,以及对后缀表达式的求值
⏹回溯算法
⏹方法调用
⏹文本编辑器中的撤销功能
⏹web浏览器的链接历史信息的保留
中缀表达式到后缀表达式的转换:
⏹从左至右读取表达式
⏹若读取的是操作数,则添加到后缀表达式中
⏹若读取的是运算符:
◆如果运算符为"(",则直接推入栈中
◆如果运算符为")",则取出栈中的右运算符,添加到后缀表达式中,直到取出左括号为止。
◆如果运算符为非括号运算符,则与栈顶的运算符做比较,如果比栈顶运算符优先级高或相等,则直接推入栈,否则取出栈中运算符,添加到后缀表达式中。
⏹当表达式读取完成,栈中还有运算符时,则依序取出栈中运算符,并分别追到后缀表达式中,直到栈为空。
后缀表达式的求值:
⏹从左至右读取表达式:
◆若读取的是操作数,则将其推入栈中
◆若是运算符,则从栈中取出两个操作数进行计算,并将结果推入栈中。
⏹重复以上步骤,直到表达式读取完毕。
队列:
线性集合,只允许在表的一端进行插入,即队尾(rear),删除则在另一端,即队头(front)。
支持先进先出(FIFO)。
队列应用:
⏹CPU访问
⏹磁盘访问
⏹打印机访问
树:
由一个或多个节点组成的有限集合。
每一颗树必须有一个特定节点,叫做根节点。
根节点下可以有零个以上的子节点。
而且各子节点也可以为子树,拥有自己的子节点。
若一棵树中的节点最多可以有n个节点,则称该树为n元树。
⏹树的相关名称
◆根节点(rootnode):
树中没有父节点的节点即为根节点
◆叶节点(leafnode):
没有子节点的节点
◆非叶节点:
叶节点以外的节点
◆父节点(parent)和子节点(child)
◆兄弟节点(sibling):
同一个父节点的节点
◆分支度(degree):
每个节点所拥有的子节点个数,树中的最大的分支度值即为该树的分支度
◆阶层(level):
根节点的阶层为1,其子节点为2,依次类推
◆高度和深度:
树的最大阶层值即为树的高度或深度
◆祖先(ancestor):
由某子节点到根节点的路径上的所有节点,均称为该节点的祖先
⏹二叉树
树的一种,节点最多只能有两个节点
◆由有限个节点组成的集合,集合可以为空
◆根节点下可有两个子树,为左子树和右子树
⏹二叉树与树的区别:
⏹二叉树可以为空,而树不可以(至少要有根节点)
⏹二叉树的子树有顺序关系
⏹二叉树的分支度必须为0、1或2,而树的分支度可以大于2
⏹二叉树类型:
⏹左(右)歪斜树
◆所有节点的左子树均不存在,则此二叉树为右歪斜树
◆反之,则称之为左歪斜树。
⏹满二叉树
◆所有叶节点均在同一阶层,其他非叶节点的分支度为2
◆若此树的高度为n,则该树的节点数为2^n-1.
⏹完全二叉树
◆一个二叉树除掉最大阶层后为满二叉树,且最大阶层的节点均向左靠齐。
⏹二叉树的节点插入规则:
◆均转换成满二叉树的形式来插入节点数据。
◆对各阶层的节点由低阶层到高阶层,从左至右,由1开始编号,再根据编号将节点数据存入相应索引编号的数组(链表)中
◆如果某编号没有节点存在,则数组对应位置没有值存入。
◆插入的第一个元素为根节点
◆满足左小右大的二叉查找树规则
提问:
依次输入数据6,3,8,5,2,9,4,7建立一个二叉树,请描述该二叉树节点的排列次序。
⏹二叉树的三种表示法:
数组表示法
见例题。
优点:
查找容易且每个节点占用空间不大
缺点:
当二叉树的深度和节点数的比例偏高时,会造成空间浪费
数据的插入和删除涉及到移动大量数据的问题
节点数组表示法
包含三个数组:
一个数组存放节点数据内容
一个数组存放左子节点在数组中的下标
一个数组存放右子节点在数组中的下标
见例题
改良后的数组表示法,在插入和删除方面需移动的数据大大减少
链表表示法
在修改二叉树方面效率比数组实现高。
⏹二叉树的遍历:
前序遍历(preordertraversal)
先遍历中间节点,再遍历左子树,最后遍历右子树
伪码表示:
If指向根节点的指针==null
Then此为空树,遍历结束
Else
(1)处理当前节点
(2)往左走,递归处理preorder(root'left)
(3)往右走,递归处理preorder(root'right)
中序遍历(inordertraversal)
先遍历左子树,再遍历中间节点,最后遍历右子树
伪码表示:
If指向根节点的指针==null
Then此为空树,遍历结束
Else
(1)往左走,递归处理preorder(root'left)
(2)处理当前节点
(3)往右走,递归处理preorder(root'right)
后序遍历(postordertraversal)
先遍历左子树,再遍历右子树,最后遍历中间节点
伪码表示:
⏹二叉树的查找:
先将二叉树转换成二叉查找树,即左小右大,接着可以采用二分查找方式来查找。
对二叉查找树的查找效率高于对非二叉查找树的查找
见例题。
⏹二叉树的删除:
见例题。
分为几种情况:
1.删除节点既无左子树也无右子树
●根节点
●非根节点
2.删除节点有左子树,无右子树
3.删除节点有右子树,无左子树
4.删除节点既有左子树,也有右子树
实例分析
数组
数组是常用的数据结构。
几乎每种编程语言里面都有该结构。
数组的优点是快速的插入数据,如果下标(索引值)知道,可以很快地存取数据。
数组的缺点是查找数据慢,删除数据慢,固定大小。
请看下例:
publicclassArrayApp{
publicstaticvoidmain(String[]args){
int[]arr;
arr=newint[100];
intnElems=0;//记录元素的个数
intj;//循环变量
intsearchKey;//要查找的数据
//插入10个元素
arr[0]=7;
arr[1]=2;
arr[2]=4;
arr[3]=5;
arr[4]=9;
arr[5]=3;
arr[6]=1;
arr[7]=0;
arr[8]=6;
arr[9]=8;
nElems=10;
System.out.println("---------遍历-----------");
for(j=0;j//打印所有的数据
System.out.print(arr[j]+"");
System.out.println("");
System.out.println("-------查找5------------");
searchKey=5;
for(j=0;jif(arr[j]==searchKey)//如果找到跳出循环
break;
if(j==nElems)//如果是通过break跳出循环,则n值不等于nElems
System.out.println("找不到"+searchKey);//yes
else
System.out.println("找到"+searchKey);//no
System.out.println("------删除6------------");
searchKey=6;//删除6
for(j=0;j//找到6的位置
if(arr[j]==searchKey)
break;
for(intk=j;k//6位置后面的数据依次往前顺移
arr[k]=arr[k+1];
nElems--;//个数减一
System.out.println("删除成功");
System.out.println("-----遍历---------------");
for(j=0;j//遍历所有数据
System.out.print(arr[j]+"");
System.out.println("");
}//endmain()
}//endclassArrayApp
上面的代码类似于C语言的编程风格。
在面向对象的编程思维里,应该按模块化的设计方式,用类来描述一个对象的信息,定义方法来封装该对象的功能,定义属性来区别不同的对象。
请看下例:
publicclassTestMyArray{
publicstaticvoidmain(String[]args){
MyArrayma=newMyArray();
ma.add(3);
ma.add
(1);
ma.add(9);
ma.add(5);
ma.add(7);
System.out.println(ma);
//------------------------------------------------
System.out.println("---------------------");
if(ma.find(5)!
=-1)
System.out.println("找到5");
else
System.out.println("没找到5");
//-------------------------------------------------
System.out.println("---------------------");
if(ma.delete(5))
System.out.println("删除成功");
else
System.out.println("删除失败");
//-----------------------------------------------
System.out.println("---------------------");
System.out.println(ma);
}//endmain()
}//endclassTestMyArray
classMyArray{
int[]arr;//声明int类型的数组的引用
intnElements;//记录数组里面元素的个数
intsize;//数组的大小
publicMyArray(){
//默认情况下,该数组对象的大小为10
this(10);
}
publicMyArray(intsize){
nElements=0;
this.size=size;
arr=newint[size];
}
//增加的方法
publicbooleanadd(intval){
if(nElements{
arr[nElements++]=val;//nElements既是下标,又记录元素个数
returntrue;
}else{
returnfalse;
}//endif
}//endadd(intval)
publicintget(intindex)//获取的方法
{
returnarr[index];
}
publicintfind(intkey)//查找的方法,如果找到返回改值所在的下标,否则返回-1
{
inti=0;
for(;i//循环查找
if(arr[i]==key)//如果找到跳出循环
break;
if(i==nElements)//如果i==nElements表示不是通过break跳出循环的,找不到!
return-1;//-1表示找不到
else
returni;//i是找到值所在的下标
}
publicbooleandelete(intkey)//删除的方法,true表示删除成功,false表示失败
{
intk=find(key);//首先先查找要删除的值存在不存在
if(k==-1){
returnfalse;
}else{
for(inti=k;iarr[i]=arr[i+1];
nElements--;
returntrue;
}//endif
}//enddelete(intkey)
publicStringtoString(){
StringBuffersb=newStringBuffer();
for(inti=0;iif(i!
=nElements-1)
sb.append(arr[i]+",");
else
sb.append(arr[i]);
}//endfor
returnsb.toString();
}
publicintgetSize()//得到该数组的大小
{
returnnElements;
}//endgetSize()
};//endMyClass
运行结果
堆栈和队列的实现
堆栈和队列都是线性表结构。
只不过堆栈的逻辑数据特点是先进后出(FILO),而队列的逻辑数据特点是先进先出(FIFO)。
我们先用数组来存放这两种数据结构,也就是线性的存储结构。
请看下例:
publicclassStackT{
privateintmaxSize;//堆栈的大小
privateint[]stackArray;
privateinttop;//堆栈顶部指针
publicStackT(ints){
maxSize=s;//初始化数组大小
stackArray=newint[maxSize];//初始化一个数组
top=-1;
}
publicvoidpush(intj)//压栈
{
if(!
isFull())//先判断是否为空
stackArray[++top]=j;
else
return;
}
publicintpop()//出栈
{
returnstackArray[top--];
}
publicintpeek()//得到栈顶的数据而不是出栈
{
returnstackArray[top];
}
publicbooleanisEmpty()//判断栈是否为空
{
return(top==-1);
}
publicbooleanisFull()//判断是否栈满
{
return(top==maxSize-1);
}
publicStringtoString()//按照堆栈的特点打印所有数据
{
StringBuffersb=newStringBuffer();
for(inti=top;i>=0;i--)
sb.append(""+stackArray[i]+"\n");
returnsb.toString();
}
}//endclassStackX
classStackApp{
publicstaticvoidmain(String[]args){
StackTtheStack=newStackT(10);//初始化一个堆栈
theStack.push(20);
theStack.push(40);
theStack.push(60);
theStack.push(80);
System.out.println(theStack);
System.out.println("");
}//endmain()
}//endclassStackApp
运行的结果如图:
队列的实现(循环队列):
publicclassQueue{
privateintmaxSize;//表示队列的大小
privateint[]queArr;//用数组来存放有队列的数据
privateintfront;//取数据的下标
privateintrear;//存数据的下标
privateintnItems;//记录存放的数据个数
publicQueue(ints){
maxSize=s;
queArr=newint[maxSize];
front=0;
rear=-1;
nItems=0;
}
publicvoidinsert(intj)//增加数据的方法
{
if(isFull())
return;
//如果下标到达数组顶部的话,让rear指向数组的第一个位置之前
if(rear==maxSize-1)
rear=-1;
queArr[++rear]=j;//incrementrearandinsert
nItems++;//onemoreitem
}
publicintremove()//删除元素
{
inttemp=queArr[front++];
//如果下标到达数组顶部的话,让front指向数组的第一个位置
if(front==maxSize)
front=0;
nIt