一、数据结构
1.数据
描述客观事物的数值、字符,能输入到计算机且被计算机处理的各种符号的集合数据就是信息在计算机中的表示
2.数据元素
数据元素是数据的基本单位
在计算机程序中,通常把数据元素作为一个整体进行处理
如:
描述学生信息的一条数据记录就是一个数据元素;描述一个点坐标的信息就是一个数据元素
数据元素通常由若干数据项组成
3.数据对象
一组相同性质的数据元素的集合
如:
学校中所有学生的集合就是数据对象;平面坐标系中所有点的集合就是数据对象
4.数据结构
相互之间存在一种或多种特定关系的数据元素的集合,数据结构就是数据元素之间的关系
数据结构分为逻辑结构和物理结构。主要研究逻辑结构
数据的逻辑结构有4钟:
集合结构:数据仅属于同一个集合,没有其他相互关系
线性结构:描述一个一对一的关系
树形结构:描述一对多的关系
图形结构:描述多对多的关系
数据的逻辑结构一般采用二元组的形式定义:
数据结构 = (D,S)
其中:
D:数据元素的集合
S:D中元素之间关系的集合
例1:
二元组:set = (D,S),其中:
D = {01,02,03,04,05,06}
S = {}
在set集合中,数据元素除了属于同一个集合外不存在其他关系,这就是集合结构
例2:
二元组:linearity = (D,S),其中:
D = {01,02,03,04,05,06}
S = {<01,04>,<04,06>,<06,02>,<02,05>,<05,03>}
在这个数据结构中,数据元素是有序的,有一个被称为“第一个”的数据元素(元素01),还有一个被称为“最后一个”的元素(元素03),除了第一个元素外,其他每个元素都有一个直接前驱,除了最后一个元素外,其他每个元素都有一个直接后继元素
数据元素之间是1对1的关系,线性关系
例3:
二元组:tree={D,S},其中:
D = {01,02,03,04,05,06}
S = {<01,02>,<01,03>,<02,04>,<02,05>,<03,06>}
在tree数据结构中,除了第一个元素外,每个元素都有且只有一个直接前驱元素,每个元素可以有多个直接后继元素
数据结构之间是1对多的关系,树形结构
例4:
二元组,graph = {D,S},其中:
D = {01,02,03,04,05,06}
S = {<01,02>,<01,03>,<02,05>,<05,06>,<06,02>,<05,04>,<04,05>}
在数据结构中,每个元素可以有多个直接前驱元素,每个元素也可以有多个直接后继元素
多对多的关系,图形结构
数据的物理结构就是逻辑结构在计算机的存储表示,他有两种表示形式:
顺序存储、链式存储
顺序存储:使用一块连续的存储空间,数据之间紧挨在一起,数据的前驱与后继的关系可以通过数据元素在内存中的相对位置反映出来
链式存储:数据元素的存储位置不是连续的,每个元素保存下一个元素的存储位置
5.抽象数据类型
数据类型:一组性质相同的数据和集合以及该数据集合上操作的总称
如:java中的int类型数据的集合:-2147483648~2147483647,在组数据上的操作,+ - * / %
抽象数据类型(ADT)abstract data type
由一组数据模型及该模型上的一组操作组成,只讨论逻辑特性
一般用三元组表示:
ADT = {D,S,P}
D是数据对象,S是D上的关系,P是D上的操作
定义抽象数据类型,可以使用以下格式:
ADT 抽象数据类型名{
数据对象;<数据对象的定义>
数据关系;<数据关系的定义>
数据操作;<基本操作的定义>
}
抽象数据类型可以对应一个java类,数据对象与数据关系可以通过类的成员变量来存储和表示,数据操作可以使用方法来实现
6.算法及性能分析
①算法
是为解决某一特定问题而规定的一系列的操作,是一组有序的指令的集合
算法特性
输入:一个算法有0个或多个输入
输出:至少有一个输出,没有输出的算法没有意义
有穷性:算法中执行指令的个数应该是有限的,执行有穷的步骤后能结束
确定性:对于特定的合法输入它的输出应该是唯一的
可行性:算法能够实现,且在有限时间内完成
算法设计要求
正确性:没有语法错误,对于合法的输入产生满足要求的输出,对于待定的输入也能产生正确的输出
可读性:算法的另一个目的是为了交流,方便阅读
健壮性:对于不合理的要求,也能给出合理的提示信息,而不是崩溃。时间效率高与存储空间小。
评价一个算法性能的好坏:实际上就是评价算法的资源占用率,计算机最重要的资源就是时间和空间
使用时间复杂度衡量程序运行需要的时间
使用空间复杂度衡量程序所占内存的大小
②时间复杂度
讨论计算机程序运行的时间可以采用以下方法:
事后统计:编程实现算法,统计所需要的时间
事前分析:采用渐近时间复杂度分析估算
渐近时间复杂度:简称时间复杂度,在进行算法分析时,语句总的执行次数,记作T(n),署关于问题规模n的函数,分析T(n)随着问题规模n的变化情况,确定T(n)的数量级
T(n) = O(f(n)). 表示随着文体规模n的增大,算法执行的时间增长率和f(n)函数的增长率相同,f(n)是问题规模n的一个函数
随着输入规模n的增大,T(n)增长越慢的算法越好
③算法时间复杂度分析
算法1:
计算1+2+3+…+n的累加和,高斯算法
public static void sum2(int n){
int sum = n*(n + 1) / 2;
System.out.println("sum == " + sum);
}
顺序执行,时间复杂度,T(n) = O(1),是常数阶
算法2:
计算1+2+3+…+n的累加和
public static void sum1(int n){
int sum = 0;
for (int i = 0;i <= n;i++){
sum += i;
}
System.out.println("sum == " + sum);
}
T(n) = O(n),线性阶
算法3:
public static void method1(int n){
int i = 1;
int count = 0;
while (i <= n){
i *= 2;
count++;
}
System.out.println("count == " + count);
}
循环控制变量i的值:1,2,4,8,16,…2*,当执行了x次,i的值为2*时,循环结束。循环条件i<=n
T(n) = O(logn)
算法4:
public static void method2(int n){
int count = 0;
int s = 0;
while (s <= n){
count++;
s += count;
}
System.out.println("count == " + count);
}
假设循环执行了x次,count变量在循环过程中的值分别是:0,1,2,3,…x
在执行完第x次后循环结束,s<=n不成立时,s的值是:s=0+1+2+…+x=x*(x+1)/2
T(n) = O(n的平方)
算法5:
public static void method3(int n){
int count = 0;
for (int i = 1;i <= n;i++){
for (int j = 1;j <= n;j++){
count++;
}
}
System.out.println("count == " + count);
}
T(n) = O(n的平方)
常见时间复杂度的增长率:
④空间复杂度
为了求解某一问题,在执行操作期间所需要的的存储空间大小,不包含用来存储输入所需要的的空间,记作:
S(n) = O(f(n))
结论:算法的空间是以时间复杂度为上限
二、线性表
1.线性表的抽象数据类型
ADT List{
数据对象:D = {ai 属于某个数据类型,i = 0,1,2,3,…}
D = {a0,a1,a2,…an},所有的元素都是同一个数据类型
数据关系:R = {<ai,ai+1>}
数据操作:
getSize():返回线性表中元素的个数
isEmpty():判断线性表是否为空,线性表为空返回true,否则返回false
insert(i,e):在线性表的i索引值位置插入元素e,如果索引值越界,会报错
contains(e):在线性表中判断是否存在元素e,存在返回true
indexOf(e):返回元素e在线性表中的索引值,如果不存在元素e,返回-1
remove(e):删除线性表中第一个与e相同的元素,删除成功返回删除的元素
remove(i):删除线性表中指定索引值的元素,返回删除的元素
replace(i,e):把线性表中索引值为i的元素替换为元素e
get(i):返回线性表中索引值为i的元素
insertBefore(p,e):在线性表中元素p的前面插入元素e
insertAfter(p,e):在线性表中元素p的后面插入元素e
}List;
抽象数据类型可以对应一个java类,数据对象与元素之间的关系可以通过成员变量来存储和表示;数据操作可以通过一组方法来实现
2.List接口
使用java中的接口来表示ADT中的数据操作,在使用累完成抽象数据类型时,只要这个类实现接口即可完成抽象数据类型中定义的操作
public interface MyList {
int getSize();//返回线性表中元素的个数
boolean isEmpty();//判断线性表是否为空
void insert(int i,Object e);//在线性表的i索引值添加元素e
boolean contains(Object e);//判断线性表中是否包含元素e
int indexOf(Object e);//返回线性表中元素e的索引值
Object remove(Object e);//删除线性表中第一个与e相同的元素,并返回该元素
Object remove(int i);//删除线性表中索引值为i的元素,并返回该元素
Object replace(int i,Object e);//使用元素e替换线性表中i位置的元素,并返回旧的元素
Object get(int i);//返回索引值为i的元素
boolean insertBefore(Object p,Object e);//在线性表元素p的前面插入元素e
boolean insertAfter(Object p,Object e);//在线性表元素p的后面插入元素e
}
3.线性表的顺序存储与实现
线性表的顺序存储就是使用一组地址连续的存储空间来一次存储线性表中的元素
以数据元素在计算机内存的地址相邻性表示数据元素之间的关系
在java中可以使用数组来存储线性表中的数据元素,数组就是一块连续的存储空间
插入:
①需要时,可以对数组扩容
②把i位置开始的元素依次后移
③把要插入的元素保存到i位置
删除:
①从i+1开始,把元素依次前移
②把最后一个元素置为null
4.实现代码
package chapter1;
/**
* @ClassName MyArrayList
* @Description 通过数组实现线性表
**/
public class MyArrayList implements MyList {
private Object [] elements;//定义数组保存数据元素
private static final int DEFAULT_CAPACITY = 16;//数组的默认初始化容量
private int size;//保存数据元素个数
//构造方法
public MyArrayList() {
elements = new Object[DEFAULT_CAPACITY];
}
public MyArrayList(int initialCapacity) {
elements = new Object[initialCapacity];
}
//返回元素个数
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
//判断线性表是否为空
return size == 0;
}
@Override
public void insert(int i, Object e) {
//在线性表的i位置插入元素e
//判断i是否越界
if (i < 0 || i >= size){
throw new IndexOutOfBoundsException(i + "越界");
}
//如果数组已满,对数组扩容
if (size > elements.length){
expandSpace();//数组扩容
}
//从i开始,把元素依次后移
for (int j = size;j > i;j--){
elements[j] = elements[j-1];
}
//把元素e存储到i位置
elements[i] = e;
//元素个数增1
size++;
}
//数组扩容
private void expandSpace(){
//定义一个更大的数组
Object[] newElements = new Object[elements.length*2];
//把原来的内容复制到新的数组中
for (int i = 0;i < elements.length;i++){
newElements[i] = elements[i];
}
//让原来的数组名指向新的数组
elements = newElements;
}
//判断当前线性表中是否包含元素e
@Override
public boolean contains(Object e) {
return indexOf(e) >= 0;
}
//返回元素e在线性表中第一次出现的索引值,如果不存在返回-1
@Override
public int indexOf(Object e) {
//遍历数组
if (e == null){
//线性表中,用户可能添加null
for (int i = 0;i < size;i++){
if (elements[i] == null){
return i;
}
}
}else {
for (int i = 0; i < size; i++) {
if (e.equals(elements[i])) {
return i;
}
}
}
return -1;
}
//删除线性表中第一个与e相同的元素
@Override
public Object remove(Object e) {
//获得e在线性表中的索引值
int index = indexOf(e);
if (index < 0){
return null;//线性表中不存在e
}
return remove(index);
}
//删除指定索引值的元素
@Override
public Object remove(int i) {
//判断i是否越界
if (i < 0 || i >= size){
throw new IndexOutOfBoundsException(i + "越界");
}
//把删除的元素保存起来
Object old = elements[i];
//把i+1依次前移
for (int j = i;j < size-1;j++){
elements[j] = elements[j+1];
}
//把最后的元素置为null
elements[size-1] = null;
//修改元素个数
size--;
//返回删除的元素
return old;
}
//把索引值为i的元素替换为e
@Override
public Object replace(int i, Object e) {
//判断索引值是否越界
if (i < 0 || i>= size){
throw new IndexOutOfBoundsException(i + "越界");
}
Object old = elements[i];//保存原来的值
//替换
elements[i] = e;
//把原来的元素值返回
return old;
}
//返回指定位置的元素
@Override
public Object get(int i) {
//判断索引值是否越界
if (i < 0 || i >= size){
throw new IndexOutOfBoundsException(i + "越界");
}
return elements[i];
}
//在指定的元素前插入一个元素
@Override
public boolean insertBefore(Object p, Object e) {
int index = indexOf(p);
//确定元素p在线性表中的位置
if (index < 0){
return false;
}
//插入元素
insert(index,e);
return true;
}
//在指定元素后插入一个元素
@Override
public boolean insertAfter(Object p, Object e) {
int index = indexOf(p);
//确定元素p在线性表中的位置
if(index < 0){
return false;
}
//插入元素
insert(index+1,e);
return true;
}
//重写toString方法
@Override
public String toString() {
//把线性表中每个元素连接起来,遍历数组中的已添加的元素
StringBuilder sb = new StringBuilder();
sb.append("[");
for (int i = 0;i < size;i++){
sb.append(elements[i]);
//数据之间使用,隔开
if (i <size-1){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
package chapter1;
/**
* @ClassName MyArrayListTest
* @Description 测试类
**/
public class MyArrayListTest {
public static void main(String[] args) {
//创建一个MyAttayList对象
MyArrayList list1 = new MyArrayList();
//判断是否为空
System.out.println(list1.isEmpty());//true
System.out.println(list1.getSize());//0
//添加元素
list1.insert(0,"aa");
list1.insert(1,"bb");
list1.insert(0,"cc");
System.out.println(list1.isEmpty());//false
System.out.println(list1.getSize());//3
//把线性表中的内容打印输出
System.out.println(list1);//[cc,aa,bb]
//判断是否存在
System.out.println(list1.indexOf("cc"));//0
System.out.println(list1.indexOf("bb"));//2
System.out.println(list1.indexOf("dd"));//-1
System.out.println(list1.contains("aa"));//true
System.out.println(list1.contains("xx"));//false
//删除
list1.remove("dd");
System.out.println(list1);
list1.insert(0,"xx");
list1.insert(0,"oo");
list1.insert(0,"yy");
System.out.println(list1); // [yy,oo,xx,aa]
list1.replace(0,"YY");
System.out.println(list1); //[YY,oo,xx,aa]
System.out.println(list1.get(0));//YY
System.out.println(list1.get(1));//oo
System.out.println(list1.get(33));//
}
}
5.顺序存储的特点
优点:使用数组实现,数组可以通过索引值快速访问每个元素
缺点:在插入或删除元素时。需要移动大量的元素。当线性表长度变化较大时,很难确定存储空间的容量
应用场景:适合存储的元素,插入/删除操作较少,主要是查询操作
6.线性表的链式存储与实现
①单向链表(单链表)
每个存储单元至少有两个存储域,一个用来存储数据,一个用来保存下个存储单元的引用
各个存储单元的地址可以是不连续的
插入/删除时不需要移动元素
②实现代码
package chapter1;
/**
* @ClassName MySingleLink
* @Description 通过单向链表实现代码
**/
public class MySingleLink implements MyList{
private Node head;//头结点
private int size;//保存元素的个数
//返回元素的个数
@Override
public int getSize() {
return size;
}
//判断线性表是否为空
@Override
public boolean isEmpty() {
return size == 0;
}
//在线性表中插入元素
@Override
public void insert(int i, Object e) {
//判断索引值是否越界
if (i < 0 || i > size){
throw new IndexOutOfBoundsException(i + "越界");
}
//创建结点
Node newNode = new Node(e,null);
//头结点为null的情况,链表不存在,刚刚添加的结点就是头结点
if (head == null){
head = newNode;
}else{
//在0位置插入结点
if (i == 0){
newNode.next = head;//修改新结点的next域指向原来的头结点
head = newNode;//刚插入的结点就是新的头结点
}else{
//插入结点,先找到i-1结点的位置
Node pNode = head;
for (int x = 1;x < i;x++){
pNode = pNode.next;
}
//注意:先修改新结点的next指针域,再修改i-1结点的指针域
newNode.next = pNode.next;
pNode.next = newNode;
}
}
//元素个数加1
size++;
}
//判断线性表中是否包含指定的元素
@Override
public boolean contains(Object e) {
return indexOf(e) >= 0;
}
//返回元素e在线性表中第一次出现的索引值
@Override
public int indexOf(Object e) {
int i = 0;//保存元素e的索引值
Node pNode = head;
while(pNode != null){
if (e == null && pNode.data == null){
return i;
}else if (e != null && e.equals(pNode.data)){
return i;
}
i++;
pNode = pNode.next;
}
return -1;
}
//从线性表中删除第一个与e相同的元素
@Override
public Object remove(Object e) {
//找到元素e第一次出现的索引值
int index = indexOf(e);
if (index < 0){
return null;//元素不存在
}
return remove(index);
}
//删除指定索引值的元素
@Override
public Object remove(int i) {
//判断是否越界
if (i < 0 || i >= size){
throw new IndexOutOfBoundsException(i + "越界");
}
Node pNode = head;
//表示删除头结点
if (i == 0){
head = head.next;
size--;
return pNode.data;//返回删除头结点的数据
}
//找到i-1结点
for (int x = 1;x < i;x++){
pNode = pNode.next;
}
Object old = pNode.next.data;//保存删除结点的数据
pNode.next = pNode.next.next;//修改i-1结点的next指针域,指向i+1结点
size--;
return old;
}
//把线性表中i索引值的元素替换为e
@Override
public Object replace(int i, Object e) {
//判断i是否越界
checkIndex(i);
//找到i结点
Node pNode = getNode(i);
Object old = pNode.data;//保存原来的数据
pNode.data = e;//替换
return old;
}
//返回线性表中i索引值的元素
@Override
public Object get(int i) {
checkIndex(i);
Node pNode = getNode(i);
return pNode.data;
}
//检查索引值是否越界
private void checkIndex(int i){
if (i < 0 ||i >= size){
throw new IndexOutOfBoundsException(i +"越界");
}
}
//定义一个方法,返回i索引值的元素
private Node getNode(int i){
if (i < 0 ||i >= size){
return null;
}
if (i == 0){
return head;
}
//找到i结点
Node pNode = head;
for (int x = 1;x <= i;x++){
pNode = pNode.next;
}
return pNode;
}
//在指定的元素p前面添加元素e
@Override
public boolean insertBefore(Object p, Object e) {
//先找p的位置
int index = indexOf(p);
if (index < 0){
return false;//元素p不存在
}
//插入元素
insert(index,e);
return true;
}
//在指定元素p后面添加元素e
@Override
public boolean insertAfter(Object p, Object e) {
int index = indexOf(p);
if (index < 0){
return false;
}
insert(index+1,e);
return true;
}
//重写toString()
@Override
public String toString() {
//把链表中各个结点的数据域连接起来
StringBuilder sb = new StringBuilder();
sb.append("[");
Node pNode = head;
while(pNode != null){
sb.append(pNode.data);
//使用逗号来分隔
if (pNode.next != null){
sb.append(",");
}
pNode = pNode.next;//指针下移
}
sb.append("]");
return sb.toString();
}
//定义一个内部类表示单向链表中的结点
private class Node{
Object data;//保存数据
Node next;//下个结点的引用
public Node(Object data,Node next){
super();
this.data = data;
this.next = next;
}
}
}
运行代码main:
package chapter1;
/**
* @ClassName MySingleLinkTest
* @Description TODO
**/
public class MySingleLinkTest {
public static void main(String[] args) {
//1.创建链表
MySingleLink link = new MySingleLink();
//2.判断
System.out.println(link.isEmpty());//true
System.out.println(link.getSize());//0
//3.插入元素
link.insert(0,"aa");
link.insert(0,"bb");
link.insert(0,"cc");
link.insert(0,"dd");
//4.直接打印输出
System.out.println(link);//[dd,cc,bb,aa]
//5.判断元素是否存在
System.out.println(link.indexOf("dd"));//0
System.out.println(link.indexOf("aa"));//3
System.out.println(link.indexOf("xx"));//-1
System.out.println(link.contains("cc"));//true
//6.删除结点
System.out.println(link.remove("xx"));//null
System.out.println(link.remove("bb"));//bb
System.out.println(link);//[dd,cc,aa]
System.out.println(link.remove(0));//dd
System.out.println(link);//[cc,aa]
//7.返回元素,元素替换
System.out.println(link.get(0));//cc
System.out.println(link.replace(0,"CC"));//cc
System.out.println(link);//[CC,aa]
//8.在指定元素的前后插入元素
link.insertBefore("aa","bb");
link.insertAfter("bb","BB");
System.out.println(link);//[CC,bb,BB,aa]
}
}
③双向链表
单向链表只能通过一个结点的引用访问它的后续结点,不能访问前驱结点,如果要找某个结点的前驱结点,需要从头结点开始依次查找
在双向链表中,扩展了结点的结构,每个结点除了存储数据外,通过一个引用指向后续结点,再定义一个引用指向前驱结点:
双向链表的结构:
④实现代码
package chapter1;
/**
* @ClassName MyDualLinkedList
* @Description 双向链表的实现
**/
public class MyDualLinkedList implements MyList{
private Node first;//头结点
private Node last;//尾结点
private int size;
//返回元素个数
@Override
public int getSize() {
return size;
}
//判断链表是否为空
@Override
public boolean isEmpty() {
return size == 0;
}
//在指定位置插入元素:分为头部、尾部、中间
@Override
public void insert(int i, Object e) {
//1.检查索引值是否越界
if (i < 0 || i >= size){
throw new IndexOutOfBoundsException(i + "越界");
}
//2.如果i == 0,在头部添加元素
if (i == 0){
addFirst(e);
}else if(i == size){
//3.如果i == size,在尾部添加元素
addLast(e);
}else{
//4.找到i结点,在i结点前面插入元素
Node pNode = getNode(i);
Node prevNode = pNode.prev;
//生成新的结点
Node newNode = new Node(e,prevNode,pNode);
//修改前驱结点的后继
prevNode.next = newNode;
pNode.prev = newNode;
size++;
}
}
//返回索引值对应的结点
private Node getNode(int i) {
//
Node pNode = first;
for (int x = 0;x < i;x++){
pNode = pNode.next;
}
return pNode;
}
//判断链表中是否包含指定的元素e,如果存在,返回true,否则返回false
@Override
public boolean contains(Object e) {
return indexOf(e) >= 0;
}
//判断元素e在链表中第一次出现的位置,如果不存在该元素返回-1
@Override
public int indexOf(Object e) {
int i = 0;//保存元素e在链表中的索引值
//依次遍历链表中的各个结点,比较结点的元素是否与e相同
if (e == null){
for (Node pNode = first;pNode != null;pNode = pNode.next){
if (pNode.data == null){
return i;
}
i++;
}
}else {
for(Node pNode = first;pNode != null;pNode = pNode.next){
if (e.equals(pNode.data)){
return i;
}
i++;
}
}
return -1;
}
//删除与e相同的第一个元素,返回删除元素
@Override
public Object remove(Object e) {
//找到元素e对应的索引值
int index = indexOf(e);
if (index < 0){
return null;
}
return remove(index);
}
//删除指定位置的元素,并返回删除元素
@Override
public Object remove(int i) {
if (i < 0 || i >= size){
throw new IndexOutOfBoundsException(i + "越界");
}
//找到i对应的结点
Node pNode = getNode(i);
Node prevNode = pNode.prev;//删除结点的前驱
Node nextNode = pNode.next;//删除结点的后继
if (prevNode == null){
//删除头结点
first = nextNode;
}else {
prevNode.next = nextNode;
}
if (nextNode == null){
//删除尾结点
last = prevNode;
}else {
nextNode.prev = prevNode;
}
size--;//修改元素个数
return pNode.data;
}
//检查索引值是否越界
private void checkIndex(int i){
if (i < 0 ||i >= size){
throw new IndexOutOfBoundsException(i +"越界");
}
}
//修改指定索引值的元素
@Override
public Object replace(int i, Object e) {
checkIndex(i);
//找到索引值为i的结点
Node pNode = getNode(i);
Object oldData = pNode.data;
pNode.data = e;
return oldData;
}
//返回指定索引值的元素
@Override
public Object get(int i) {
checkIndex(i);
//找到索引值为i的结点
Node pNode = getNode(i);
return pNode.data;
}
//在指定元素p前插入元素e
@Override
public boolean insertBefore(Object p, Object e) {
//找到p元素位置
int index = indexOf(p);
if (index < 0){
return false;
}
insert(index,e);
return true;
}
//在指定元素p后插入元素e
@Override
public boolean insertAfter(Object p, Object e) {
//找到p元素位置
int index = indexOf(p);
if (index < 0){
return false;
}
insert(index+1,e);
return true;
}
//重写toString方法
@Override
public String toString() {
//依次遍历各个结点,把元素转换为字符串
StringBuilder sb = new StringBuilder();
sb.append("[");
for (Node node = first;node != null;node = node.next){
sb.append(node.data);
//元素之间使用逗号分隔
if (node != last){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
//在链表中,经常会有针对头结点与尾结点的操作
//在尾部添加元素e
public void addLast(Object e) {
Node pNode = last;
//生成一个新结点
Node newNode = new Node(e,last,null);
if (pNode == null){
first = newNode;
}else {
pNode.next = newNode;
}
last = newNode;
size++;
}
//在头部添加元素e
public void addFirst(Object e) {
Node pNode = first;
//生成一个新结点
Node newNode = new Node(e,null,first);
first = newNode;//修改first指向新的结点
if (pNode == null){
last = newNode;
}else {
pNode.prev = newNode;
}
size++;
}
//删除第一个元素,删除头结点
public Object removeFirst(){
return remove(0);
}
//删除最后一个元素(尾结点)
public Object removeLast(){
return remove(size-1);
}
//返回头结点
public Object getFirst(){
return get(0);
}
//返回尾结点
public Object getLast(){
return get(size-1);
}
//定义一个内部类描述双向链表的特点
private class Node{
Object data;
Node prev;//指向前驱结点
Node next;//指向后继结点
public Node(Object data, Node prev, Node next) {
this.data = data;
this.prev = prev;
this.next = next;
}
}
}
测试代码main:
package chapter1;
/**
* @ClassName Test
* @Description 测试类
**/
public class Test {
public static void main(String[] args) {
//创建双向链表
MyDualLinkedList linkedList = new MyDualLinkedList();
System.out.println(linkedList.getSize());//0
System.out.println(linkedList.isEmpty());//true
linkedList.insert(0,"gg");//[gg]
linkedList.insert(0,"jj");//[jj,gg]
linkedList.insert(1,"dd");//[jj,dd,gg]
linkedList.insert(3,"mm");//[jj,dd,gg,mm]
System.out.println(linkedList);
//测试是否存在某个元素
System.out.println(linkedList.indexOf("jj"));//0
System.out.println(linkedList.indexOf("mm"));//3
System.out.println(linkedList.indexOf("DD"));//-1
System.out.println(linkedList.contains("xx"));//false
//删除指定结点
System.out.println(linkedList.remove(0));
System.out.println(linkedList);
linkedList.insertBefore(0,"xx");
System.out.println(linkedList.get(1));
System.out.println(linkedList);//[xx,gg]
linkedList.insertBefore("xx","yy");
linkedList.insertBefore("xx","oo");
System.out.println(linkedList);
linkedList.addFirst("TT");
linkedList.addLast("TT");
System.out.println(linkedList);//[TT,yy,xx,oo,gg,MM]
System.out.println(linkedList.getFirst());
System.out.println(linkedList.getLast());
}
}
7.顺序存储与链式存储实现线性表的比较
①时间上比较
线性表的基本操作:查询、插入、删除
查询:
数组顺序存储–直接通过索引值访问每个元素,实现了数组元素的随机访问
链式存储:每次从头结点或者尾结点开始一次查找
如果线性表主要是查询操作,优先选择顺序存储的线性表
插入与删除:
数组顺序实现的线性表,在插入/删除时,需要移动大量元素
链式存储,只需要修改结点的前驱后继指针即可,不需要移动元素
如果线性表经常用于插入/删除操作,优先选择链式存储实现的线性表
②空间上比较
顺序存储:
预先分配一块连续的存储空间,在使用过程中会出现闲置空间
链式存储:
是动态分配,不会浪费空间
如果线性表的长度经常变化,优先选择链式存储。
如果线性表的长度变化不大时,优先选择顺序存储,因为链式存储需要额外的空间存储前驱和后继
三、栈
1.栈的特点
栈(Stack),也叫堆栈,是一种操作受限的线性表,栈只允许在线性表的一端进行插入/删除等操作,不允许在其他位置插入/删除
在线性表中进行插入/删除的一端称为栈顶(top),栈顶保存的元素称为栈顶元素,相对的另一端称为栈底(bottom)
如果栈中没有数据元素称为空栈。向栈中插入元素,称为进栈/入栈,从栈中删除元素,称为退栈/出栈
栈的插入/删除操作只允许在栈顶进行,后进栈的元素必定先出栈,称为先进后出(FILO)
结构图:
2.堆栈抽象数据类型的定义
ADT Stack{
数据对象:D = {a0,a1,a2,…an,ai是同一种数据类型的元素}
数据关系:R = {<ai,ai+1>}
基本操作:
getSize():返回元素个数
isEmpty():判断栈是否为空
push(Object):压栈,入栈
pop():弹栈,出栈
peek():返回栈顶元素
}ADT Stack;
顺序栈可以通过数组存储堆栈的元素,堆栈的操作都在栈顶完成,选择数组中索引值较大的一端作为栈顶
3.栈的顺序实现
package chapter2;
import java.util.Arrays;
/**
* @ClassName MyArrayStack
* @Description 堆栈的顺序实现
**/
public class MyArrayStack implements MyStack{
private Object[] elements;//定义数组保存堆栈的元素
private static final int DEFAULT_CAPACITY = 16;//堆栈的默认容量
private int top;//栈顶指针
//在无参构造中,对数组默认初始化
public MyArrayStack() {
elements = new Object[DEFAULT_CAPACITY];
}
//在构造方法中,指定对栈的初始化大小
public MyArrayStack(int initialCapacity) {
elements = new Object[initialCapacity];
}
//返回元素个数
@Override
public int getSize() {
return top;
}
//判断堆栈是否为空
@Override
public boolean isEmpty() {
return top <= 0;
}
//入栈/压栈
@Override
public void push(Object e) {
//判断堆栈是否已满,若已满,则需要扩容
if (top >= elements.length){
//1.定义一个更大的数组
Object[] newData = new Object[elements.length*2];
//2.把原来的数组内容复制到大数组中
for (int i = 0;i < elements.length;i++){
newData[i] = elements[i];
}
//3.让原来的数组名指向新的数组
elements = newData;
}
//把元素存储到栈顶指针指向的位置
elements[top] = e;
//栈顶指针上移
top++;
}
//出栈
@Override
public Object pop() {
//判断堆栈是否为空
if(top <= 0){
throw new StackOverflowError("栈已空");
}
top--;//栈顶指针下移
return elements[top];
}
//返回栈顶元素,不删除
@Override
public Object peek() {
if (top <= 0){
throw new StackOverflowError("栈已空");
}
return elements[top-1];
}
//重写toString方法
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
//从栈顶到栈底的顺序添加各个元素
for (int i = top-1;i >= 0;i--){
sb.append(elements[i]);
if (i > 0){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
}
测试代码:
package chapter2;
/**
* @ClassName Test
* @Description TODO
**/
public class Test {
public static void main(String[] args) {
//1.创建堆栈
MyArrayStack stack = new MyArrayStack();
System.out.println(stack.getSize());//0
System.out.println(stack.isEmpty());//true
//2.压栈
stack.push("aa");
stack.push("bb");
stack.push("cc");
stack.push("dd");
System.out.println(stack.getSize());//4
System.out.println(stack.isEmpty());//false
System.out.println(stack);//[dd,cc,bb,aa]
System.out.println(stack.peek());//dd
System.out.println(stack.pop());//dd
System.out.println(stack);//[cc,bb,aa]
}
}
4.栈的链式实现
使用链表作为栈的存储结构,有时也称为链栈,栈只允许在线性表的一端进行操作,可以选择链表头部作为栈顶
不管是入栈还是出栈,都在链表的首结点上进行
package chapter2;
/**
* @ClassName MyLinkStack
* @Description 栈的链式存储
**/
public class MyLinkStack implements MyStack{
private Node top;//存储栈顶的引用
private int size;//保存堆栈中元素的个数
//返回堆栈元素的个数
@Override
public int getSize() {
return size;
}
//判断堆栈是否为空
@Override
public boolean isEmpty() {
return size == 0;
}
//入栈操作
@Override
public void push(Object e) {
//根据元素生成结点,插入到链表头部
Node pNode = new Node(e,top);
//修改top栈顶指针指向新的结点
top = pNode;
size++;
}
//出栈
@Override
public Object pop() {
//先判断堆栈是否为空
if (size < 1){
throw new StackOverflowError("栈空");
}
Object old = top.data;
top = top.next;
size--;
return old;
}
//返回栈顶元素
@Override
public Object peek() {
//先判断堆栈是否为空
if (size < 1){
throw new StackOverflowError("栈空");
}
return top.data;
}
//重写toString方法
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
for (Node pNode = top;pNode != null;pNode = pNode.next){
sb.append(pNode.data);
//数据元素之间使用逗号隔开
if (pNode.next != null){
sb.append(",");
}
}
sb.append("]");
return sb.toString();
}
//定义一个内部类,描述链表中的结点
private class Node{
Object data;//存储数据
Node next;//存储下一个结点的引用
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
}
}
测试代码:
package chapter2;
/**
* @ClassName Test2
* @Description TODO
* @Author wj
* @DATE 2021/1/16 16:14
**/
public class Test2 {
public static void main(String[] args) {
//1.创建链栈
MyLinkStack stack = new MyLinkStack();
System.out.println(stack.isEmpty());
System.out.println(stack.getSize());
//2.压栈
stack.push("ppp");
stack.push("aaa");
stack.push("xxx");
stack.push("ooo");
//3.
System.out.println(stack);
//4.出栈
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack.pop());
System.out.println(stack);
}
}
5.栈的应用
栈的特点:后进先出
①进制转换
十进制转二进制:对2取余数,倒序输出
实现代码:
package chapter2;
/**
* @ClassName TestBaseConversion
* @Description 使用栈完成进制转换
**/
public class TestBaseConversion {
public static void main(String[] args) {
//
System.out.println(convert(100,2));
}
/*
*把一个十进制整数num转换为decimal指定的进制
*/
public static String convert(int num,int decimal){
MyArrayStack stack = new MyArrayStack();//保存余数
int remainder = num % decimal;//余数
while (num != 0){
stack.push(remainder);//余数压栈
num = num / decimal;
remainder = num % decimal;
}
//出栈,余数倒序
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()){
sb.append(stack.pop());
}
return sb.toString();
}
}
②检测表达式中括弧是否匹配
假设表达式中包含三种括弧:小括弧(),中括弧[],大括弧{},这三种括弧可以任意嵌套
算法:
读取整个表达式,如果是左括弧就直接入栈,等待与它对应的右括弧出现;
如果是右括弧,则与当前栈顶的左括弧判断是否匹配
如果不匹配,说明表达式不合法
如果是右括弧,栈已空,表示不合法
读完整个表达式,堆栈不为空,表示有左括弧,没匹配上,表达式不合法,
读完整个表达式,栈是空的,表示所有括弧都能匹配
package chapter2;
/**
* @ClassName TestBracketMatch
* @Description TODO
**/
public class TestBracketMatch {
public static void main(String[] args) {
System.out.println(bracketMatch("([{}])"));
System.out.println(bracketMatch("([{(}])"));
System.out.println(bracketMatch("([{})])"));
}
public static boolean bracketMatch(String expression){
MyArrayStack stack = new MyArrayStack();//保存左括弧
//遍历整个表达式,如果是左括弧就入栈,如果是右括弧,就出栈进行判断是否匹配
for (int i = 0;i < expression.length();i++){
//取出表达式的每个字符
char cc = expression.charAt(i);
switch (cc){
case '(':
case '[':
case '{':
stack.push(cc);//左括弧入栈,java可以自动装箱与拆箱
break;
case '}':
if (!stack.isEmpty() && stack.pop().equals('{')){
break;
}else {
return false;
}
case ']':
if (!stack.isEmpty() && stack.pop().equals('[')){
break;
}else {
return false;
}
case ')':
if (!stack.isEmpty() && stack.pop().equals('(')){
break;
}else {
return false;
}
}
}
//表达式遍历完后,如果栈是空的,表示括弧匹配
if (stack.isEmpty()){
return true;
}else {
return false;
}
}
}
③算术表达式的求值
四则运算的规则:
先乘除后加减、先括弧内再括弧外、从左到右进行运算
算法思路:
1)定义两个栈,一个存储操作符operator,一个存储操作数operand
2)读取表达式,如果是操作数就存储到operand操作数栈
如果是操作符:
(1)操作符栈是空,直接入栈
(2)把操作符栈中的栈顶操作符与当前操作符进行优先级比较
当前操作符优先级高,操作符入栈
当前操作符优先级低(栈顶操作符优先级高),弹出栈顶操作符,从操作数栈中弹出两个操作数进行运算,吧运算结果存储到操作数栈中,继续判断当前操作符与栈顶操作符的优先级
(3)遍历完整个表达式,两个栈都不为空,依次弹出操作符operator栈中的运算符与操作数栈中的两个操作数进行计算,把结果再存储到操作数栈中
(4)如果操作符栈不为空,或操作数栈中的数不止有一个,则表达式错误
package chapter2;
/**
* @ClassName TestCalculateExpression
* @Description 使用栈计算算术表达式的值
**/
public class TestCalculateExpression {
public static void main(String[] args) {
String expression = "4+3+(6-10+2*3)*4";
double result = calculate(expression);
System.out.println(result);
}
//定义方法计算指定表达式的值
private static double calculate(String expression) {
MyArrayStack operatorStack = new MyArrayStack();//存储操作符
MyArrayStack operandStack = new MyArrayStack();//存储操作数
//遍历表达式中的操作数与操作符
for (int i = 0;i < expression.length();i++){
char cc = expression.charAt(i);
//如果cc是数字
if (Character.isDigit(cc)){
//取出操作数
StringBuilder sb = new StringBuilder();
//只要是数字就是操作数的一部分
while (Character.isDigit(cc)){
sb.append(cc);
i++;
if (i >= expression.length()){
//表达式结束
break;
}
cc = expression.charAt(i);//去下个字符
}
//操作数入栈
operandStack.push(sb.toString());
//修正i变量的值
i--;
//System.out.println(sb);
}else {
//如果是操作符
//1.栈为空
if (operatorStack.isEmpty()){
operatorStack.push(cc);
continue;
}
//2.操作符栈不为空的情况
while(!operatorStack.isEmpty()) {
char op1 = (char) operatorStack.peek();
//判断栈中运算符与当前运算符的优先级
if (compareOperator(op1, cc) < 0) {
//当前运算符的优先级高于栈顶运算符的优先级
operatorStack.push(cc);
break;
} else if (compareOperator(op1, cc) == 0) {
//当前运算符的优先级等于栈顶运算符的优先级,只有一种情况,左一半小括弧遇到右一半小括弧的情况
operatorStack.pop();//栈中左一半小括弧出栈
break;
} else {
//栈顶运算符优先级高
double num1 = Double.parseDouble(operandStack.pop().toString());
double num2 = Double.parseDouble(operandStack.pop().toString());
//取栈顶运算符
char operator =(char) operatorStack.pop();
//计算num2 op num1
double result = compute(operator,num2,num1);
//把结果存储到操作数栈中
operandStack.push(result);
//如果当前操作符栈为空,新的操作符入栈
if (operatorStack.isEmpty()){
operatorStack.push(cc);
break;
}
}
}
}
}
//当表达式遍历完后,如果操作符栈不为空,需要继续计算
while (!operatorStack.isEmpty()){
char operator = (char) operatorStack.pop();
double num1 = Double.parseDouble(operandStack.pop().toString());
double num2 = Double.parseDouble(operandStack.pop().toString());
double result = compute(operator,num2,num1);
operatorStack.push(result);
}
//当操作符栈为空,操作数栈中多于一个数,表达式错误
if (operandStack.getSize() > 1){
throw new RuntimeException("表达式错误");
}
return Double.parseDouble(operandStack.pop().toString());
}
//计算num1 op num2表达式的值
private static double compute(char operator, double num2, double num1) {
switch (operator){
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
return num1 / num2;
}
return 0;
}
//判断两个运算符的优先级,如果op1优先级高返回正数,op2优先级高返回负数
private static int compareOperator(char op1, char op2) {
if (op1 == '+' || op1 == '-'){
if (op2 == '*' || op2 == '/' || op2 == '('){
//第一个运算符是+-,第二个运算符是*/(
return -1;
}
}
if (op1 == '*' || op1 == '/'){
if (op2 == '('){
//第一个运算个是*/,第二个运算符是(
return -1;
}
}
if (op1 == '('){
if (op2 == ')'){
return 0;
}else {
return -1;
}
}
return 1;
}
}
四、队列(Queue)
1.队列的特点
队列简称为队,也是一种受限的线性表,只允许在线性表的一端进行插入,而在表的另一端进行删除,称插入数据的一端为队尾(rear),称删除数据的一端为队首(front)
向队列添加数据称为入队或进队,新入队的元素称为队尾元素
向队列删除元素称为出队或离队,元素出队之后,它的后续元素称为新的队首元素
队列是一种先进先出(FIFO)表
2.队列抽象数据类型的定义
ADT Queue{
数据对象:D = {a0,a1,a2,…an,ai都是同一种数据类型的元素}
数据关系:R = {<ai,ai+1>}
数据操作:
getSize():返回元素个数
isEmpty():判断队列是否为空
enQueue(e):入队
deQueue(e):出队
peek():返回队首元素
}ADT Queue
3.队列的顺序存储
在循环实现中,可以把数据设想为一个圆环,这种数组称为循环数组,用循环数组实现的队列称为循环队列
用front指针指向队首元素所在的单元,使用rear指针指向队尾元素所在单元的后一个单元
在元素入队时,将新入队的元素保存到rear指向的单元,然后rear指向后移;在出队时,将队首指针front指向的元素返回,front后移
一般情况下,采用两种方式表示队列已满:
①少用一个存储单元,当队尾指针rear的下个单元是队首指针front时,停止入队。
(rear+1)%capacity == front时表示队列满,当front == rear时表示队列为空
②增设一个标志表示队列为空还是满,通常用size变量表示元素的个数,当size == 0时队列为空,当size == capacity时表示队列已满
package chapter3;
/**
* @ClassName MyArrayQueue
* @Description 队列的顺序存储实现
**/
public class MyArrayQueue {
private Object[] elements;//定义数组存储
private static final int DEFAULT_CAPACITY = 8;
private int front;//队首
private int rear;//队尾
private int size;//元素个数
//构造方法
public MyArrayQueue() {
elements = new Object[DEFAULT_CAPACITY];
}
public MyArrayQueue(int initialCapacity) {
elements = new Object[initialCapacity];
}
//返回元素的个数
public int getSize(){
return size;
}
//判断队列是否为空
public boolean isEmpty(){
return size == 0;
}
//入队
public void enQueue(Object e){
//如果队列已满,可以对数组扩容
if (size >= elements.length){
expandQueue();
}
elements[rear] = e;//把元素存储到rear指针指向的单元
rear = (rear+1)%elements.length;//rear指针后移
size++;//元素个数加1
}
//队列的数组进行扩容
private void expandQueue(){
//定义一个更大的数组
Object[] newElements = new Object[elements.length*2];
//把原来的数组内容复制到新的数组中
for (int i = 0;i < size;i++){
newElements[i] = elements[front];
front = (front+1)%elements.length;
}
//让原来的数组名指向新的数组
elements = newElements;
//调整新的队首与队尾指针
front = 0;
rear = size;
}
//出队
public Object deQueue(){
//如果队列为空
if (size <= 0){
//抛出一个队列为空的异常
throw new QueueEmptyException("队列为空");
}
//队列不为空,把front指向的元素返回
Object old = elements[front];
front = (front+1)%elements.length;//front指针后移
size--;
return old;
}
//返回队首元素
public Object peek(){
//队列为空,抛出异常
if (size <= 0){
throw new QueueEmptyException("队列为空");
}
return elements[front];
}
}
测试代码:
package chapter3;
/**
* @ClassName TestQueue
* @Description 测试顺序队列
**/
public class TestQueue {
public static void main(String[] args) {
MyArrayQueue queue = new MyArrayQueue();
queue.enQueue("a");
queue.enQueue("b");
queue.enQueue("c");
queue.enQueue("d");
System.out.println(queue.peek());//a
System.out.println(queue.deQueue());//a
System.out.println(queue.deQueue());//b
System.out.println(queue.deQueue());//c
System.out.println(queue.deQueue());//d
queue.enQueue("1");
queue.enQueue("2");
queue.enQueue("3");
queue.enQueue("4");
queue.enQueue("5");
queue.enQueue("6");
queue.enQueue("7");
queue.enQueue("8");
queue.enQueue("9");
queue.enQueue("J");
queue.enQueue("Q");
queue.enQueue("K");
System.out.println(queue.deQueue());//1
}
}
4.队列的链式存储
使用单向链表来实现队列
把链表的头部作为队首,把链表的尾部作为队尾
package chapter3;
/**
* @ClassName MyLinkQueue
* @Description 队列的链式存储
**/
public class MyLinkQueue {
private Node front;//队首
private Node rear;//队尾
private int size;//元素个数
//返回元素个数
public int getSize(){
return size;
}
//判断队列是否为空
public boolean isEmpty(){
return size == 0;
}
//入队
public void enQueue(Object e){
//根据添加的元素生成一个结点
Node newNode = new Node(e,null);
//把结点连接到队列中
if (rear == null){
//这是添加的第一个元素,既是头结点也是尾结点
rear = newNode;
front = newNode;
}else {
//把结点连接到队列尾部
rear.next = newNode;
rear = newNode;//rear指针指向新添加的元素
}
size++;//元素个数+1
}
//出队
public Object deQueue(){
//判断队列是否为空
if (size <= 0){
throw new QueueEmptyException("队列为空");
}
Object old = front.element;
front = front.next;
//如果出队后,队列为空,调整尾指针
if (front == null){
rear = null;
}
size--;
return old;
}
//返回队首元素
public Object peek(){
if (size <= 0){
throw new QueueEmptyException("队列为空");
}
return front.element;
}
//通过内部类表示单向链表的结点
private class Node{
Object element;
Node next;
public Node(Object element, Node next) {
this.element = element;
this.next = next;
}
}
}
五、树
1.树的定义
树是由一个集合及该集合上定义的一种关系构成的,集合中的元素称为树的结点。定义的关系称为父子关系,父子关系的树的结点之间建立一个层次结构
树的递归定义:
树(Tree)是由n(n>=0)个结点组成的有限集,当n = 0时,称为空树,不包含任何结点;当n>0时,就是一棵非空树,(1)有且仅有一个特定的称为根的结点(root);(2)当n>1时,其他节点可以分为m(m>0)个互不相交的有限集T1,T2…,其中每个有限集本身又是一棵树,称为根结点的子树(Sub Tree)
在右图中,结点A是根结点,它包含T1和T2两棵子树,T1={BDGHI},T2={CEFJ},每棵子树又是一棵树,在T1子树中,B是根结点,在T2子树中,C是根结点
注意:
当n>0时,在非空树中,根结点唯一
在m>0时,某个结点的子树是没有限制的,且各个子树肯定是不相交的
2.相关概念
结点拥有的子树的数量称为结点的度(Degree)
度为0的结点称为叶子结点(Leaf)或者终端结点,度不为0的结点称为分支结点或非终端节点
除了根结点外,分支结点也称为内部结点,树的度是树内各个结点中度的最大值
结点的子树的根称为该结点的孩子(Child),相应的该结点称为孩子结点的双亲(Parent)结点或父结点
父子结点之间的连线是树的一条边,树中结点数等于树的边数加1
在树中,根结点没有双亲结点,其他结点都有且只有一个父结点,每个结点可以有多个孩子结点
同一个双亲的孩子之间互称为兄弟(Sibling)
结点的祖先是从根结点到该结点所经过的分支上所有的结点
以某结点为根的子树的任一结点都称为该结点的子孙
结点的层次(Level)是从根结点开始,根为第1层,根的孩子为第2层,依次类推。
注意,有人把层次的定义是从0开始的,即根为第0层
如果某结点在第i层,则其子树的根就在i+1层
双亲结点(父结点)在同一层次上的结点弧尾堂兄弟,即DEF互为堂兄弟
树中结点的最大层次称为树的深度(Depth)或高度,当前树的高度是4
路径:在树中k+1个结点通过k条边构成的序列称为长度为k的路径,在图中{(D,B),(B,A),(A,C),(C,E)}构成了一条连接D结点与E结点的路径,该路径的长度为4。在树中任意两个结点都有唯一的路径,从根结点开始,存在到其他任一结点的唯一路径
有序树、无序树:如果将树的结点的各个子树看作是从左到右有顺序的,不能互换的,则称该树为有序树,否则称无序树。如果不特殊说明,一般讨论的是有序树
树中所有节点最大度数为m的有序树称为m叉树
森林(Forest)是m(m>=0)棵互不相交的树的集合。对树的每个结点而言,其子树的集合就是森林,删去树的根就得到一个森林,反之,把森林加一个树的根就变成一棵树
3.树的抽象数据类型
ADT Tree{
数据对象:D是具有相同性质的数据元素的集合
数据关系:如果D是空,则R是空;如果D不为空,D中存在唯一一个称为根的元素root,该元素没有前驱;除了根元素外,D中每个元素都有且只有一个前驱
数据操作:
getSize():返回元素个数
getRoot():返回树的根元素
getParent(x):返回x结点的父结点
getFirstChild(x):返回x结点的第一个孩子
getNextSibling(x):返回x结点的下个兄弟结点,如果x是最后一个孩子,返回null
getHeight(x):返回以x结点为根的树的高度
insertChild(x,child):将结点child为根的子树插入到当前树中,作为x结点的孩子
deleteChild(x,i):删除结点x的第i棵子树
preOrder(x):先序遍历x为根的树
inOrder(x):中序遍历x为根的树
postOrder(x):后序遍历x为根的树
levelOrder(x):按层次遍历x为根的树
}ADT Tree
4.树的存储结构
①双亲表示法
树中的结点,除了根结点外,都有一个双亲结点,可以在只用数组存储树中的每个结点,数组的下标就是数组的位置指针,每个结点在增加一个指向双亲的指针域,结点的结构可以定义为:
存储结构:
在双亲表示法存储结构中,可以方便的通过parent指针域找到该结点的父结点,如果想找某个结点的孩子结点,需要遍历整个数组
可以在结点中再增加一个长子域,指向第一个孩子的指针域,如果没有孩子,这个长子域设置为-1
如:
②孩子表示法
树中每个结点可能有多棵子树,可以考虑使用多重链表,每个结点可以有多个指针域,每个指针域指向它的子树的根结点,把这种方法称为多重链表表示法
树的每个结点的度可能不一样,即每个结点的孩子个数可能不相等,一般设计以下两种方案:
方案一:
结点中指针域的个数就是树的度(树中结点最多的孩子树),如:
结点中孩子域的个数就是树的度
如果树中各个结点的度相差很大时,很浪费空间,有很多结点的指针域是空的,这种表示方法适合树的各个结点度相关很少的情况
方案二:
每个结点的指针域个数等于该结点的度,在结点中专门定义一个存储该结点度的域
如结点可以设计为:
这种方法提高了空间利用率,但各个结点的结构可能不一样,还要维护结点的度的值,会增加时间上损耗
可以定义一个线性表存储树中的所有结点的信息,称为结点表,每个结点建立一个孩子表,孩子表只存储孩子结点的在数组中的存储位置,由于每个结点的孩子结点的个数是不确定的,经常使用一个链表表示孩子之间的关系,叫作,孩子表示法
上述例子可以表示为:
在这种表示法中,需要设计两种结点,一个结点表数组中表头结点,包括数据域和指向第一个孩子的指针域,如:
还需要设计一个孩子结点,存储孩子结点在数组的下标和只想下个孩子结点的指针,如:
在这种结构中,可以方便查找某个结点的孩子,也可以方便查找某个结点的兄弟,只需要访问这个结点的孩子链表即可,如果需要查找结点的父结点,还需要遍历整棵树。可以在结点表中即数组中的结点增加一个指向父结点的指针,如:
这种表示法为:双亲孩子表示法
③孩子兄弟表示法
从树结点的兄弟的角度来确定树的存储结构,对于任意一棵树,它的结点的第一个孩子如果存在肯定是唯一的,如果结点的右兄弟存在也肯定是唯一的,可以设置两个指针,分别指向某个结点的第一个孩子和它的右兄弟,如:
例子表示:
这种表示法,可以方便查找某个结点的孩子和兄弟,把一棵复杂的树转换为一棵二叉树
5.二叉树(Binary Tree)
是由n个结点组成的集合。该集合要么是空集合,要么是一个由根结点和两棵互不相交的二叉树组成
特点:
①每个结点最多有两棵子树
②左子树与右子树有顺序
③即使树中的某个结点只有一个子树,也是区分左子树与右子树的
二叉树的五种基本形态:
空二叉树、只有一个结点的二叉树、根结点只有左子树、根结点只有右子树、根结点既有左子树又有右子树
特殊的二叉树
①斜树:所有结点只有左子树的二叉树称为左斜树,所有结点都只有右子树的二叉树称为右斜树
②满二叉树:在一棵二叉树中,如果所有分支节点都有左子树和右子树,且所有叶子结点都在同一层上,这样的二叉树称为满二叉树,即每层的结点都是满的
满二叉树的特点:
1)叶子结点只能出现在最下面一层
2)非叶子结点的度一定是2
3)在同样深度的二叉树中,满二叉树的结点树是最多的,叶子也是最多的
③完全二叉树:对一棵具有n个结点的二叉树,按层次编号,如果编号为i的结点与同样深度的满二叉树编号为i的结点在二叉树中的位置完全相同,这就是一棵完全二叉树。就是满二叉树最下层从最右侧开始去掉相邻的若干叶子结点
满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满的
完全二叉树的特点:
1)叶子结点只能出现在最下两层
2)最下层的叶子结点集中在左侧连续的位置
3)倒数第二层的叶子结点一定都在右边连续的位置
4)如果结点的度为1,则该结点只有左孩子
5)同样结点树的二叉树,完全二叉树的深度最小
6.二叉树的性质
①在二叉树的第i层上,最多有 (2的i次方-1)个结点
②深度为k的二叉树,最多有 (2的k次方-1)个结点
③对于任意一棵二叉树,叶子结点的数量n0,度为2的结点数量n2,则n0 = n2+1
④具有n个结点的完全二叉树深度为floor(log2n)+1
满二叉树深度为k,结点总数量:2的k次方-1,如果吧总结点的数量记为n,即n = 2的k次方-1,则k = log2(n+1)
深度为k的完全二叉树结点数量n一定小于等于同样深度的满二叉树的结点数,一定大于深度为k-1的满二叉树结点的数量,即 2的k-1次方-1 < n <= 2的k次方-1。n就是深度为k的完全二叉树结点的数量
⑤对于一个完全二叉树进行按层次编号,
对于任意一个结点i有:
如果i == 1,则结点i是二叉树的根;如果i>1,则该结点的双亲结点是i/2
如果2i > n,则结点i没有左孩子,否则左孩子是2i
如果2i + 1 > n,则结点i没有右孩子,否则右孩子是2i+1
7.二叉树的存储结构
①顺序存储
使用一维数组存储二叉树中的结点,结点中的存储位置(数组的下标)可以反映结点之间的逻辑关系
完全二叉树的顺序存储,对完全二叉树的各个结点按层次编号
将完全二叉树存储到数组中,数组的下标对应存储位置
如果不是完全二叉树,可以将二叉树编号,把不存在的结点设置为null
如果二叉树中有很多不存在的结点,会造成存储空间的浪费,一般情况下,顺序存储只用于完全二叉树
②链式存储
二叉树的结点最多有两个孩子,可以为结点设计一个数据域,一个指向左孩子的指针域和一个右孩子的指针域,由这样的结点组成的链表称为二叉链表
二叉树结点的结构可以设计为:
例:
为了方便找到父结点,可以在结点上增加一个指向父结点的指针域,这种结点组成的链表称为三叉链表,结点的结构可以设计为
上面的二叉树使用三叉链表可以表示为:
③三叉链表的遍历(代码实现)
package chapter4;
/**
* @ClassName BinaryTreeNode
* @Description 三叉链表的结点
**/
public class BinaryTreeNode {
private Object data;//数据域
private BinaryTreeNode parent;//父结点指针域
private BinaryTreeNode lChild;//左孩子指针域
private BinaryTreeNode rChild;//右孩子指针域
private int height;//以当前结点为根结点的二叉树高度
private int size;//以当前结点为根结点的二叉树所有结点的数量
/*************构造方法********************/
//根据指定的数据创建一个结点
public BinaryTreeNode(Object e) {
data = e;
parent = null;
lChild = null;
rChild = null;
height = 1;//二叉树高度从1开始
size = 1;
}
public BinaryTreeNode() {
this(null);
}
/*************构造方法********************/
//判断是否有父结点
public boolean hasParent(){
return parent != null;
}
//判断是否有左孩子
public boolean hasLChild(){
return lChild != null;
}
//判断是否有右孩子
public boolean hasRChild(){
return rChild != null;
}
//判断是否为叶子结点
public boolean isLeaf(){
return lChild == null && rChild == null;
}
//判断是否为父结点的左孩子
public boolean isLChild(){
return parent != null && parent.lChild == this;
}
//判断是否为父结点的右孩子
public boolean isRChild(){
return parent != null && parent.rChild == this;
}
/*************与height高度相关的操作*******************/
//返回高度
public int getHeight(){
return height;
}
//更新当前结点的高度以及祖先结点的高度
public void updateHeight(){
int newHeight = 0;//保存新的高度
//当前结点的高度为左子树的高度或右子树的高度,较大的那个加1
if (hasLChild()){
newHeight = Math.max(newHeight,getLChild().getHeight()+1);
}
if (hasRChild()){
newHeight = Math.max(newHeight,getRChild().getHeight()+1);
}
//如果当前结点高度有变化,递归更新祖先结点的高度
if (newHeight == height){
//刚计算出的高度与原来一样
return;
}
//把新的高度作为当前结点的高度
height = newHeight;
//更新祖先结点的高度
if (hasParent()){
getParent().updateHeight();
}
}
/*************与结点个数相关的操作*******************/
//返回以当前结点为根的二叉树的结点数
public int getSize(){
return size;
}
//更新当前结点及祖先的结点数
public void updateSize(){
size = 1;//当前结点本身
//累加左子树的结点数
if (hasLChild()){
size += getLChild().getSize();
}
//累加右子树的结点数
if (hasRChild()){
size += getRChild().getSize();
}
//递归更新祖先结点数
if (hasParent()){
getParent().updateSize();
}
}
/*************与父结点相关的操作*******************/
//返回父结点
public BinaryTreeNode getParent(){
return parent;
}
//断开与父结点的关系
public void disInheritence(){
//如果没有父节点
if (!hasParent()){
return;
}
//
if (isLChild()){
//当前结点是父结点的左孩子
parent.lChild = null;
}else if (isRChild()){
//当前结点是父结点的右孩子
parent.rChild = null;
}
//更新父结点的高度
parent.updateHeight();;
//更新父结点的结点数
parent.updateSize();
//修改当前结点的父结点的指针
parent = null;
}
/*************与左孩子相关的操作*******************/
//返回左孩子
public BinaryTreeNode getLChild(){
return lChild;
}
//设置当前结点的左孩子。把原来的左孩子返回
public BinaryTreeNode setLChild(BinaryTreeNode newLChild){
BinaryTreeNode oldLChild = this.lChild;//保存原来的左孩子
//先断开当前结点的左孩子
if (hasLChild()){
lChild.disInheritence();
}
//设置新的左孩子为参数结点
if (newLChild != null){
newLChild.disInheritence();//先把参数结点断开与原来父结点的关系
this.lChild = newLChild;//把参数结点设置左孩子
newLChild.parent = this;//设置参数结点的父结点
this.updateHeight();
this.updateSize();
}
return oldLChild;//返回原来的左孩子结点
}
/*************与右孩子相关的操作*******************/
//返回右子树
public BinaryTreeNode getRChild(){
return rChild;
}
//设置右孩子
public BinaryTreeNode setRChild(BinaryTreeNode newRChild){
BinaryTreeNode oldRChild = this.rChild;//保存原来的右孩子
//先断开当前结点的右孩子
if (hasRChild()){
rChild.disInheritence();
}
//设置新的右孩子为参数结点
if (newRChild != null){
newRChild.disInheritence();
this.rChild = newRChild;
newRChild.parent = this;
this.updateHeight();
this.updateSize();
}
return oldRChild;
}
}
8.二叉树的遍历
从根结点出发,按照某种次序一次访问二叉树中所有结点,使得每个结点被访问一次且只访问一次
按照结点被访问的次序可以得到,由二叉树所有结点组成的一个序列
①前序遍历(先序遍历、先根序遍历)–DLR
如果二叉树为空,则是空操作,否则先访问根结点,前序遍历左子树,前序遍历右子树
②中序遍历(LDR)
如果二叉树为空,则是空操作;否则,中序遍历左子树,访问根节点,中序遍历右子树
③后序遍历(LRD)
如果二叉树为空,则是空操作;否则,后序遍历左子树,后序遍历右子树,访问根
④层序遍历
从树的第一层,即从根结点开始访问,从上到下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问
⑤实现代码
package chapter4;
import java.util.LinkedList;
/**
* @ClassName BinaryTree
* @Description 使用三叉链表创建二叉树
**/
public class BinaryTree {
private BinaryTreeNode root;//二叉树的根结点
public BinaryTree(BinaryTreeNode root) {
this.root = root;
}
//返回元素的个数
public int getSize(){
if (root == null){
return 0;//空树
}
return root.getSize();
}
//判断二叉树是否为空
public boolean isEmpty(){
return getSize() == 0;
}
//返回根结点
public BinaryTreeNode getRoot(){
if (root != null){
return root;
}
return null;
}
//返回树的高度
public int getHeight(){
if (root == null){
return 0;
}
return root.getHeight();
}
/************二叉树先序遍历***************************/
//先序遍历二叉树,把遍历的结点存储到List列表中
private void perOrderRecursion(BinaryTreeNode root,LinkedList<Object> list){
if (root == null){
return ;//空树
}
//先访问根结点,把根结点存储到列表中
list.add(root.getParent());
//递归,先序遍历左子树
perOrderRecursion(root.getLChild(),list);
//递归,先序遍历右子树
perOrderRecursion(root.getRChild(),list);
}
//打印当前二叉树的先序遍历序列
public void preOrder(){
LinkedList<Object> list = new LinkedList<>();
perOrderRecursion(root,list);
System.out.println(list);
}
/************二叉树中序遍历***************************/
private void inOrderRecursion(BinaryTreeNode root,LinkedList<Object> list){
if (root == null){
return ;//空树
}
//递归,先序遍历左子树
inOrderRecursion(root.getLChild(),list);
//先访问根结点,把根结点存储到列表中
list.add(root.getParent());
//递归,先序遍历右子树
inOrderRecursion(root.getRChild(),list);
}
//打印当前二叉树的先序遍历序列
public void inOrder(){
LinkedList<Object> list = new LinkedList<>();
perOrderRecursion(root,list);
System.out.println(list);
}
/************二叉树后序遍历***************************/
private void postOrderRecursion(BinaryTreeNode root,LinkedList<Object> list){
if (root == null){
return ;//空树
}
//递归,先序遍历左子树
postOrderRecursion(root.getLChild(),list);
//先访问根结点,把根结点存储到列表中
list.add(root.getParent());
//递归,先序遍历右子树
postOrderRecursion(root.getRChild(),list);
}
//打印当前二叉树的先序遍历序列
public void postOrder(){
LinkedList<Object> list = new LinkedList<>();
perOrderRecursion(root,list);
System.out.println(list);
}
/************二叉树层序遍历***************************/
public void levelOrderTraverse(BinaryTreeNode root,LinkedList<Object> list){
if (root == null){
return;
}
//定义一个队列,存储结点
LinkedList<BinaryTreeNode> queue = new LinkedList<>();
queue.offer(root);//根结点入队
while (!queue.isEmpty()){
//把队列头部的结点取出来
BinaryTreeNode node = queue.poll();//出队
//把结点的数据添加到list列表中
list.add(node.getParent());
//分别把node的左结点和右结点入队
if (node.hasLChild()){
queue.offer(node.getLChild());
}
if (node.hasRChild()){
queue.offer(node.getRChild());
}
}
}
public void levelOrder(){
LinkedList<Object> linkedList = new LinkedList<>();
levelOrderTraverse(root,linkedList);
System.out.println(linkedList);
}
}
测试代码:
package chapter4;
/**
* @ClassName Test
* @Description TODO
**/
public class Test {
public static void main(String[] args) {
//创建根结点
BinaryTreeNode root = new BinaryTreeNode("oo");
//创建两个结点设置为root根结点的左孩子和右孩子
BinaryTreeNode xx = new BinaryTreeNode("xx");
BinaryTreeNode yy = new BinaryTreeNode("yy");
root.setLChild(xx);
root.setRChild(yy);
//创建两个结点,作为xx结点的左孩子和右孩子
BinaryTreeNode xl = new BinaryTreeNode("xll");
BinaryTreeNode xr = new BinaryTreeNode("xrr");
xx.setRChild(xl);
xx.setLChild(xr);
//创建一个结点作为yy结点的右孩子
BinaryTreeNode yr = new BinaryTreeNode("yrr");
yy.setRChild(yr);
//创建二叉树
BinaryTree tree = new BinaryTree(root);
tree.preOrder();
//先序遍历
tree.preOrder();
//中序遍历
tree.inOrder();
//后序遍历
tree.postOrder();
//层序遍历
tree.levelOrder();
}
}
9.二叉树遍历练习