1.数据结构和算法的关系
- 数据data结构是一门研究组织数据方式的学科,
- 程序=数据结构+算法
- 数据结构是算法的基础
2.线性结构和非线性结构
2.1线性结构
- 线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性关系。
- 线性结构有两种不同的存储结构,即顺序存储结构(数组)和链式存储结构(链表)。顺序存储的线性表成为顺序表,顺序表中的存储元素是连续的。
- 链式存储的线性关系称为链表,链表中的存储元素不一定连续,
- 线性结构常见的有:数组,队列,链表和栈。
2.2非线性链表
非线性链表包括:二维数组,多维数组,广义表,树结构,图结构。
3,稀疏数组和对列
因为二维数组的很多值是默认值0,或者一个值得数组时,可以使用稀疏数组。
稀疏数组的处理方法是:
- 记录数组一共有几行几列,有多少不同的值
- 把具有不同之的元素的行列及值记录在一个小规模的数组中,从而缩小程序的规模
稀疏数组的举例说明:
二维数组转换 稀疏数组思路:
- 遍历原始的二维数组,得到有效数据的个数sum
- 根据sum就可以创建稀疏数组
- 将二维数组中的值存入稀疏数组
代码:
1.先遍历二维数组,获取二维数组中非0数据的个数
int sum=0;
//假设数组为a[][]
//X表示行数,Y标识列数
for(int i=0;i<X;i++){
for(int j=0;j<Y;j++){
if (a[i][j]!=0){
sum++;
}
}
}
2.创将对应的稀疏数组
//int[sum+1][3]-->[3]第一列表示二维数组的行数,第二列表示二维数组的列数,第三列表示非零个数。
int sparseArr[][]=new int[sum+1][3];
sparseArr[0][0]=X;
sparseArr[0][1]=Y;
sparseArr[0][3]=sum;
//遍历二维数组,将非0的值存入spareArr中
int count=0;
//记录是第几个非0数据
for(int i=0;i<X;i++){
for(int j=0;j<Y;j++){
if(a[i][j]!=0){
count++;
sparseArr[count][0]=i;
sparseArr[count][1]=j;
sparseArr[count][3]=a[i][j];
}
}
}
稀疏数组转换 二维数组思路:
- 先读取稀疏数组第一行,根据第一行的数据闯将二维数组。
- 再读取稀疏数组后几行的数据,并赋值给二维数组。
代码:
//1.定义数组b存放二维数组
int b[][]=new int[sparseArr[0][0]][sparse[0][1]];
//2.在读取稀疏数组后的几行数据(从第二行开始),并赋值肥原二维数组即可
for(int i=1;i<sparseArr.length;i++){
b[sparseArr[i][0]][sparseArr[i][1]]=sparseArr[i][2];
}
4.队列
4.1队列介绍
- 队列是一个有序列表,可以用数组或是链表来实现。
- 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的数据要后取出。
4.2.1数组模拟对队列思路
- 队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列中maxSize是该队列的最大容量。
- 因为队列的输出,输入是分别从前后端来处理,因此需要两个变量front 及rear分别记录队列前后端的下标,front会随着数据输出而改变,而rear则是随着数据输入而改变,
- 当我们将数据存入队列时称为“addQueue”,addQueue的处理需要有两个步骤:思路分析
- 将尾指针往后移:rear+1,当front==rear 空
- 若尾指针rear小于队列的最大下标maxSize-1,则将数据存入rear所指的数组元素中,否则无法存入数据。当rear==maxSize-1 队列满
代码:
(用数组模拟)
class ArrayQueue{
private int maxSize;//表示数组的最大容量
private int front;//队列头
private int rear;//队列尾
private int[] arr;//该数组用于存放数据,模拟队列
//创建队列的构造器
public ArrayQueue(int arrMaxSize){
maxSize=arrMaxSize;
arr=new int[maxSize];
front=-1;//指向队列头部,分析出front是指向队列头的前一个位置
rear=-1;//指向队列尾,指向队列尾,指向队列尾的数据(最后一个数据)
}
//判断队列是否满
public boolean isFull(){
return rear==maxSize-1;
}
//判断队列是否为空
public boolean isEmpty(){
return rear==front;
}
//添加数据到队列
public void addQueue(int n){
//判断队列是否满
if(isFull()){
System.out.println("队列满,不能加入数据");
return;
}
rear++;//让rear后移
arr[rear]=n;
}
//获取队列的数据,出队列
public int getQueue(){
if(isEmpty()){
//通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
front++;//front后移
return arr[front];
}
//显示队列的所有数据
public void showQueue(){
//遍历
if(isEmpty()){
System.out.println("队列空的,没有数据!!");
return;
}
//输出
for(int i=0;i<arr.length;i++){
System.out.printf("arr[%d]=%d\n",i,arr[i]);
}
}
//显示队列的头数据,注意不是取出数据
public int headQueue(){
//判断
if(isEmpty()){
throw new RuntimeException("队列空的,没有数据!!");
}
return arr[front+1];
}
}
#缺点:
该数组只能使用一次,没有达到复用效果。
4.2.2数组模拟对环形队列思路
对4.2.1中的数组模拟队列的优化,充分利用数组,因此将数组看作是一个环形的。(通过取模的方法来实现)
分析说明:
- 尾索引的下一个为头索引时表示队列满,即将队列容量空出一个作为约定,这个再做判断队列满的时候需要注意(rear+1)%maxSize==front 则满。
- rear=front 则空。
思路:
- front变量的含义做一个调整:front就指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素front的初始值=0;
- rear变量的含义做一个调整:rear指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定,rear的初始值=0;
- 当队列满时,条件是(rear+1)%maxSize=front 满
- 对队列为空的条件,rear==front 空
- 当我们这样分析,队列中有效的数据的个数 (rear+maxSize-front)%maxSize //rear=1;front=0
代码:
(用数组模拟)
class CircleArray{
private int maxSize;//表示数组的最大容量
private int front;//指向队列的第一个元素,也就是说arr[front]就是队列的第一个元素,
//front的初始值=0
private int rear;//队列尾
//rear变量的含义:rear指向队列的最后一个元素的后一个位置,因为希望空出一个空间作为约定。
//rear的初始值=0
private int[] arr;//该数组用于存放数据,模拟队列
//创建队列的构造器
public CircleArray(int arrMaxSize){
maxSize=arrMaxSize;
arr=new int[maxSize];
}
//判断队列是否满
public boolean isFull(){
return (rear+1)%maxSize==front;
}
//判断队列是否为空
public boolean isEmpty(){
return rear==front;
}
//添加数据到队列
public void addQueue(int n){
//判断队列是否满
if(isFull()){
System.out.println("队列满,不能加入数据");
return;
}
//直接将数据加入
arr[rear]=n;
//将rear后移,(考虑取模)
rear=(rear+1)%maxSize;
}
//获取队列的数据,出队列
public int getQueue(){
if(isEmpty()){
//通过抛出异常
throw new RuntimeException("队列空,不能取数据");
}
//1.先把front对应的值保留到一个临时变量
//2.将front后移,考虑取模
//3.将临时变量返回
int value=arr[front];
front=(front+1)%maxSize;
return value;
}
//显示队列的所有数据
public void showQueue(){
//遍历
if(isEmpty()){
System.out.println("队列空的,没有数据!!");
return;
}
//输出
for(int i=0;i<front+size();i++){
System.out.printf("arr[%d]=%d\n",i%maxSize,arr[i%maxSize]);
}
}
//求出当前队列有效数据的个数
public int size(){
return (rear-front+maxSize)%maxSize;
}
//显示队列的头数据,注意不是取出数据
public int headQueue(){
//判断
if(isEmpty()){
throw new RuntimeException("队列空的,没有数据!!");
}
return arr[front];
}
}
5.链表
5.1链表介绍
链表是有序的列表,其在内存中的存储如图:
- 链表是以节点的方式来存储
- 每个节点包含data域,next域:指向下一个节点
- 链表的各个节点不一定是连续存储
- 链表分为带头结点的链表和没有带头结点的链表
5.2单链表的应用
使用带head头结点的单向链表完成(水浒英雄)的增删改查操作
5.2.1定义节点
//定义HeroNode,每个HeroNode对象就是一个节点
class HreoNode{
public int no;
public String name;
public String nickname;
public HeroNode next;//指向下一节点
//构造器
public HreoNode(int no,String name,String nickname){
this.no=no;
this.name=name;
this.nickname=nickname;
}
//重写toString方法
public String toString(){
return "HeroNode[no="+no+",name="+name+",nickname="+nickname+]";
}
}
5.2.2定义头节点
//定义SingleLinkedList管理英雄
calss SingleLinkedList{
//先初始化一个头结点,头节点不要动,不存放具体的数据
private HeroNode head=new HreoNode(0,"","");
}
5.2.3添加节点
5.2.3.1添加在链表尾部
//定义SingleLinkedList管理英雄
calss SingleLinkedList{
//先初始化一个头结点,头节点不要动,不存放具体的数据
private HeroNode head=new HreoNode(0,"","");
//5.2.3.1添加在链表尾部
//思路:1,找到当前链表的最后一个节点。2,将最后一个节点的next指向新的节点
public void add(HeroNode heronode){
//因为head节点不能动,因此我们需要一个辅助遍历temp
HeroNode temp=head;
//遍历链表,
while(true){
//找到链表的最后
if(temp.next==null){
break;
}
//如果没有找到
temp=temp.next;
}
//当推出while循环时,temp就指向了链表的最后
//将最后这个节点的next指向新的节点
temp.next=hreroNode;
}
}
5.2.3.2插入到指定位置
//定义SingleLinkedList管理英雄
calss SingleLinkedList{
//先初始化一个头结点,头节点不要动,不存放具体的数据
private HeroNode head=new HreoNode(0,"","");
//5.2.3.2插入到指定位置
//思路:1,找到当前链表的最后一个节点。2,将最后一个节点的next指向新的节点
public void add(HeroNode heronode){
//因为head节点不能动,因此我们需要一个辅助遍历temp
HeroNode temp=head;
boolean flag=false;//flag标志表示添加的编号是否存在,默认为false
//遍历链表,
while(true){
//找到链表的最后
if(temp.next==null){
break;
}
if(temp.next.no>heroNode.no){
//位置找到,就在temp的后面插入
break;
}else if(temp.next.no==hreoNode.no){
//说明希望添加的标号已经存在
flag=true;//说明编号存在
break;
}
//如果没有找到
temp=temp.next;
}
//判断flag的值
if(flag){
System.out.printf("不能添加");
}else{
//插入到链表中temp节点的后面
heroNode.next=temp.next;
temp.next=heroNode;
}
}
}
5.2.3修改节点
//定义SingleLinkedList管理英雄calss SingleLinkedList{ //先初始化一个头结点,头节点不要动,不存放具体的数据 private HeroNode head=new HreoNode(0,"",""); //修改节点的信息,根据no编号来修改, 1.根据newHeroNode的no来修改即可 public void update(HeroNode newHeroNode){ //判断是否为空 if(head.next==null){ System.out.println("链表为空"); return; } //定义辅助变量temp找到需要修改的节点 HeroNode temp=head.next; boolean flag=false; while(true){ if(temp==null){ break;//已经遍历完链表 } if(temp.no==newHeroNode.no){ flag=true; break; } temp=temp.next; } //根据flag判断是否找到要修改的节点 if(flag){ temp.name=newHeroNode.name; temp.nickname=newHeroNode.nickname; }else{ System.out.println("没有找到"); } }}
5.2.5删除节点
//定义SingleLinkedList管理英雄calss SingleLinkedList{ //先初始化一个头结点,头节点不要动,不存放具体的数据 private HeroNode head=new HreoNode(0,"",""); //删除节点 //思路 /* 1.head不能动,因此需要一个temp辅助节点找到待删除结点的前一个结点 2.用temp.next.no与要删除节点的no比较 */ public void del(int no){ HeroNode temp=head; boolean flag=false; while(true){ if(temp.next==null){ break; } if(temp.next.no=no){ flag=true; break; } temp=temp.next; } if(flag){ temp.next=temp.next.next; }else{ System.out.println("没有找到"); } }}
5.2.6显示链表
//定义SingleLinkedList管理英雄calss SingleLinkedList{ //先初始化一个头结点,头节点不要动,不存放具体的数据 private HeroNode head=new HreoNode(0,"",""); //5.2.6显示链表 public void list(){ //判断链表是否为空 if(head.next==null){ return; } //因为头节点不能动,因此我们需要一个辅助节点便利 HeroNode temp=head.next; while(true){ if(temp==null){ break; } System.out.println(temp); temp=temp.next; } }}
5.3单链表应用案例
5.3.1求单链表的有效节点个数
public static int getLength(HeroNode head){ if(head.next==null){ return 0; } int length=0; HeroNode temp=head.next; while(temp!=null){ length++; temp=temp.next; } return length;}
5.3.2查找单链表中倒数第k个元素
思路:
- 编写方法,接收head节点,同时接受一个index
- index表示的是倒数第几个节点
- 先把链表从头到尾便利,得到链表的长度 getLength()
- 得到size后,从链表第一个节点便利(size-index)
public static HeroNode findLastIndexNode(HeroNode head,int index){ if(head.next==null){ return null; } //遍历得到链表长度 int size=getLength(head); //得到size后,从链表第一个节点便利(size-index) if(index<=0||index>size){ return null; } //xunhuan HeroNode temp=head.next; for(int i=0;i<size-index;i++){ temp=temp.next; } return temp;}
5.3.3单链表的反转
思路:
- 先定义一个节点 reverseHead=new HeroNode();
- 从头到尾遍历,去除最后一个节点插入到最前端
- 原来的链表head.next=rexerseHead.next
//单链表的反转public static void reverseList(HeroNode head){ //如果当前链表为空或只有一个节点,直接返回 if(head.next==null||head.next.next==null){ return; } //遍历 HeroNode temp=head.next; HeroNode next=null;//指向当前节点的下一节点 HeroNode reverseHead=new HeroNode(0,"",""); //从头到尾遍历,去除最后一个节点插入到最前端 while(temp!=null){ next=temp.next;//当亲节点的下一节点 temp.next=reverseHead.next;//将temp的下一节点指向新链表的最前端 reverseHead.next==temp;//连接到新链表 temp=next;//后移 }}
缺点:
- 单向链表,查找的方向只能是一个方向,
- 单链表不能自我删除,需要靠辅助变量
5.4双向链表应用案例
5.4.1创建一个双向链表的类
//定义HeroNode,每个HeroNode对象就是一个节点class HreoNode{ public int no; public String name; public String nickname; public HeroNode next;//指向下一节点 public HreoNode pre; //构造器 public HreoNode(int no,String name,String nickname){ this.no=no; this.name=name; this.nickname=nickname; } //重写toString方法 public String toString(){ return "HeroNode[no="+no+",name="+name+",nickname="+nickname+]"; }}
5.4.2添加在链表尾部
//定义SingleLinkedList管理英雄calss SingleLinkedList{ //先初始化一个头结点,头节点不要动,不存放具体的数据 private HeroNode head=new HreoNode(0,"",""); //5.4.2添加在链表尾部 //思路:1,找到当前链表的最后一个节点。2,temp.next=newHeroNode;newHeroNode.pre=temp. public void add(HeroNode heronode){ //因为head节点不能动,因此我们需要一个辅助遍历temp HeroNode temp=head; //遍历链表, while(true){ //找到链表的最后 if(temp.next==null){ break; } //如果没有找到 temp=temp.next; } //当推出while循环时,temp就指向了链表的最后 //将最后这个节点的next指向新的节点 temp.next=hreroNode; heroNode.pre=temp; }}
5.4.3修改节点
//定义SingleLinkedList管理英雄calss SingleLinkedList{ //先初始化一个头结点,头节点不要动,不存放具体的数据 private HeroNode head=new HreoNode(0,"",""); //修改节点的信息,根据no编号来修改, 1.根据newHeroNode的no来修改即可 public void update(HeroNode newHeroNode){ //判断是否为空 if(head.next==null){ System.out.println("链表为空"); return; } //定义辅助变量temp找到需要修改的节点 HeroNode temp=head.next; boolean flag=false; while(true){ if(temp==null){ break;//已经遍历完链表 } if(temp.no==newHeroNode.no){ flag=true; break; } temp=temp.next; } //根据flag判断是否找到要修改的节点 if(flag){ temp.name=newHeroNode.name; temp.nickname=newHeroNode.nickname; }else{ System.out.println("没有找到"); } }}
5.4.4删除节点
//定义SingleLinkedList管理英雄calss SingleLinkedList{ //先初始化一个头结点,头节点不要动,不存放具体的数据 private HeroNode head=new HreoNode(0,"",""); //删除节点 //思路 /* 1.直接找到要删除节点 2.自我删除 */ public void del(int no){ if(head.next==null){ return; } HeroNode temp=head.next; boolean flag=false; while(true){ if(temp==null){ break; } if(temp.no=no){ flag=true; break; } temp=temp.next; } if(flag){ temp.pre.next=temp.next; //防止空指针异常 if(temp.next!=null){ temp.next.pre=temp.pre; } }else{ System.out.println("没有找到"); } }}
5.4.5显示链表
//定义SingleLinkedList管理英雄calss SingleLinkedList{ //先初始化一个头结点,头节点不要动,不存放具体的数据 private HeroNode head=new HreoNode(0,"",""); //5.2.6显示链表 public void list(){ //判断链表是否为空 if(head.next==null){ return; } //因为头节点不能动,因此我们需要一个辅助节点便利 HeroNode temp=head.next; while(true){ if(temp==null){ break; } System.out.println(temp); temp=temp.next; } }}
5.5单向环形链表
5.5.1简介
? 环形单链表和普通单链表几乎一样,唯一不同的就是普通单链表的最后一个节点的next为空,而环形单链表的最后一个节点的next为头节点,形成一个闭环。
5.5.2创建节点对象
/*节点对象*/ class Node { private int no; private Node next; public Node(int no) { this.no = no; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public Node getNext() { return next; } public void setNext(Node next) { this.next = next; } @Override public String toString() { return "Node{" + "no=" + no + '}'; } }
5.5.3创建链表对象
/*单向环形链表*/ class CircleSingleLinkedList { private Node first = null; }
5.4.4判断是否为空
/*是否为空的方法*/ public boolean isEmpty() { return first == null; }
5.4.5增加节点到尾部
/*增加一个节点到尾部的方法*/ public boolean addNode(Node node) { /*如果头为空,那么就将头赋值,并且将头的next指向自己,达到环形的效果*/ if (isEmpty()) { first = node; first.setNext(first); return true; } /*辅助节点*/ Node tmp = first; while (true) { /*如果当前节点的next为头节点,那么说明它就是尾部节点了。*/ if (tmp.getNext() == first) { /*那么当前节点的next就等于我们新添加的节点*/ tmp.setNext(node); /*既然新添加的节点是在最后面,那么新添加的节点的next就是头节点*/ node.setNext(first); return true; } /**/ tmp = tmp.getNext(); } }
- 如果当前链表为空,那么直接把node赋值给我们链表的头节点。
- 否则就遍历,然后遍历到最后一个条件的条件是当前节点的下一个节点等于头节点,那么就说明当前节点就是最后一个节点了。
- 然后找到最后一个节点后,最后一个节点的next等于我们当前加入的节点,需要注意的是当前加入的节点的next要为头节点。
注意:
? 这里遍历到尾部的条件和加入节点后尾部节点的next处理和普通单链表不一致。因为要形成闭环,那么尾部节点的next一定要为first,否则就闭不了。
6.栈
6.1栈的介绍
- 栈是一个先入后出的有序列表、
- 栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top ),另一端为固定的一段,称为栈底(Bottom ).
6.2栈的应用场景
- 子程序的调用:在跳往子程序前,会先将下个指令的地址存放到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。
- 处理递归调用:和子程序的调用类似,只是除了存储下一个指令的地址外,也将参数,区域变量等数据存入堆栈中。
- 表达式的转换
- 二叉树的遍历
- 图形的深度优先算法
6.3栈的快速入门
6.3.1用数组模拟栈
思路:
- 使用数组模拟栈
- 定义一个top来表示栈顶,初始化为-1
- 入栈的操作:当有数据加入到栈时,top++;stack[top]=data;
- 出栈的操作,int value=stack[top];top–;return value;
6.3.1.1定义栈的类
//定义一个ArrayStack表示栈class ArrayStack{ private int maxSize;//栈的大小 private int[] stack;// 模拟栈 private int top =-1;//表示栈顶,初始化为-1 //构造器 public ArrayStack(int maxSize){ this.maxSize=maxSize; stack=new int[this.maxSize]; } //栈满 public boolean isFull(){ return top==maxSize-1; } //栈空 public boolean isEmpy(){ return top=-1; } // 入栈 public void push(int value){ if(isFull()){ System.out.println("栈满"); return; } top++; stack[top]=value; } //出栈 public void pop(){ if(isEmpty()){ throw new RuntimeException("栈空"); } int value=stack[top]; top--; return value; } //显示栈的情况 public void list(){ if(isEmpty()){ System.out.println("栈空"); return; } //从栈顶显示 for(int i=top;i>=0;i--){ System.out.printf("stack[%d]=%d\n",i,stack[i]); } }}
6.3 栈实现综合计算器(中缀表达式)
用栈来实现综合计算器-
? 7x2x2-5+1-5+3-4=?
思路:
- 通过一个index值(索引),来遍历表达式
- 如果发现是一个数字,就直接入栈
- 如果发现扫描到的是一个符号,就分情况
- 如果发现当前的符号栈为空,就直接入栈
- 如果当前符号栈有符号,就进行比较,如果当前操作符的优先级小于或等于栈中的操作符,就需要从数栈在pop出两个数,从符号栈中pop一个操作符,进行运算,结果压入数栈,然后将当前操作符push符号栈,如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈。
- 当表达式扫描完毕,就顺序的从数栈和符号栈pop数据进行运算
- 最后在数栈中只有一个数字,就是表达式的结果
public calss Calculator{ public static void main(String[] args){ //根据前面老师思路,完成表达式 String exception="7x2x2-5+1-5+3-4"; //创建两个栈,数栈,符号栈 ArrayStack numStack=new ArrayStack(10); ArrayStack operStack=new ArrayStack(10); //定义相关变量 int index=0;//用于扫描 int num1=0; int num2=0; int oper=0; int res=0; char ch='';//将每次扫描到的char保存到ch String keepNum="";//用于拼接多位数 //开始循环扫描expression while(rrue){ //依次得到expression的每个字符 ch=expression.substring(index,index+1).charAt(0); // 判断ch是什莫,做相应处理 if(operStack.isOper(ch)){ //运算符 //判断当前的符号栈是否为空 if(!operStack.isEmpty()){ //如果当前操作符的优先级小于或等于栈中的操作符,就需要从数栈在pop出两个数,从符号栈中pop一个操作符,进行运算, if(operStack.priority(ch)<=operStack.priority(operStack.peek())){ num1=numStack.pop(); num2=numStack.pop(); oper=operStack.pop(); res=numStack.cal(num1,num2,oper); //结果入栈 numStack.push(res); //压入当前运算符 operStack.push(ch); }else{ //如果当前的操作符的优先级大于栈中的操作符,就直接入符号栈。 operStack.push(ch); } }else{ //如果为空直接如符号栈 operStack.push(ch); } }else{ //如果是数,则直接入数栈 /* 分析思路: 1。处理多位数时,不能发现是一个数立即入栈,可能是多位数 2.需要向expression的表达式index后一位看,如果是数就进行扫描,符号直接入栈 3.定义keepNum 用于拼接字符串 */ keepNum+=ch; //如果ch已经是expression的最后一位,就直接入栈 if(index==expression.length()-1){ numStack.push(Integer.parseInt(keepNum)); }else{ //判断下一字符是不是数字,如果是数字,就继续扫描,如果是运算符,则直接入站 if(operStack.isOper(expression.substring(index+1,index+2).charAt(0))){ //如果后一位是运算符 numStack.push(Integer.parseInt(keepNum)); //每次清空keepNum keepNum=""; } } } //让index+1,并判断是否扫描到expression最后 index++; if(index>=expression.length()){ break; } } //当扫描完毕,就顺序从数栈和符号栈pop出相应的书和符号 while(true){ //如果符号栈为空,数栈只有一个结果 if(operStack.isEmpty()){ break; } num1=numStack.pop(); num3=numStack.pop(); oper=operStack.pop(); res=numStack.cal(num1,num2,oper); //结果入栈 numStack.push(res); } int res2=numStack.pop(); System.out.printf("%s=%d",expression,res2); }}//创建栈(参考6.3.1.1定义栈的类)//返回运算符的优先级public int priority(int oper){ if(oper=='x'||oper=='/'){ return 1; }else if(oper=='+'||oper=='-'){ return 0; }else{ return -1; }}//判断是不是开一个运算符public boolean iaOper(char val){ return val=='+'||val=='-'||val=='x'||val=='/';} //计算方法public int cal(int num1,int num2,int oper){ int res=0;//保留结果 switch(oper){ case '+': res=num1+num2; break; case '-': res=num2-num1; break; case 'x': res=num1xnum2; break; case '-': res=num2/num1; break; default: break; } return res;}
6.4逆波兰计算器
我们完成一个逆波兰计算器,要求完成如下任务:
- 输入一个逆波兰表达式(后缀表达式),使用栈,计算其结果
- 支持括号和多位数整数
思路:
例如:(30+4)x5-6对应的后缀表达式就是30 4 +5 x 6 -,求职步骤如下:
- 从左至右扫描,将30,4 压入堆栈
- 遇到+运算符,因此弹出30,4,计算器结果,将结果压入数栈
- 将5入栈
- 接下来是x运算符,弹出5,7,计算器结果,将结果压入数栈
- 将6入栈
- 最后是-运算符,计算器结果,将结果压入数栈
代码:
定义类->将表达式存入ArrayList中
public class PolandNotation{ public static void main(String[] args){ //先定义逆波兰表达式 //(30+4)x5-6对应的后缀表达式就是30 4 +5 x 6 -, String suffixExpression="30 4 +5 x 6 -"; //将表达式存入ArrayList中 List<String> list=getListString(suffixExpression); System.out.println("renList="+list); int res=calcuate(list); System.out.println("结果"+res); }
分割字符串
//分割字符串 //将一个逆波兰表达式依次将数据和运算符放入ArrayList中 public static List<String> getListString(String suffixExpression){ //将suffixExpression分开 String[] split=suffixExpression.split(" "); List<String> list=new ArrayList<String>(); for(String ele:split){ list.add(ele); } return list; }
逆波兰表达式的运算
public static int calculate(List<String> ls){ //创建栈 Stack<String> stack=new Stack<String>(); //遍历ls for(String item:ls){ if(item.matches("\\d+")){ stack.push(item); }else{ int num2=Integer.parseInt(stack.pop()); int num1=Integer.parseInt(stack.pop()); int res=0; if(item.equals("+")){ res=num1+num2; }else if(item.equals("-")){ res=num1-num2; }else if(item.equals("x")){ res=num1xnum2; }else if(item.equals("/")){ res=num1/num2; }else{ throw new RuntimeException("运算符有误"); } stack.push(""+res); } return Integer.parseInt(stack.pop()); }}
}
6.5中缀表达式转换为后缀表达式
? 大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发 中,我们需要将 中缀表达式转成后缀表达式。
具体步骤如下:
-
初始化两个栈:运算符栈 s1 和储存中间结果的栈 s2; -
从左至右扫描中缀表达式; -
遇到操作数时,将其压 s2; -
遇到运算符时,比较其与 s1 栈顶运算符的优先级: 1.如果 s1 为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈; 2.否则,若优先级比栈顶运算符的高,也将运算符压入 s1; 3.否则,将 s1 栈顶的运算符弹出并压入到 s2 中,再次转到(4-1)与 s1 中新的栈顶运算符相比较; -
遇到括号时:
-
如果是左括号“(”,则直接压入 s1 2.如果是右括号“)”,则依次弹出 s1 栈顶的运算符,并压入 s2,直到遇 到左括号为止,此时将这一对括号丢弃 -
重复步骤 2 至 5,直到表达式的最右边 -
将 s1 中剩余的运算符依次弹出并压入 s2 -
依次弹出 s2 中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
代码:
//将中缀表达式转换为后缀表达式 public static List<String> parseSuffixExpressionList(List<String> ls){ //定义栈 Stack<String> s1=new Stack<String>();//符号栈 //定义队列 List<String> s2=new ArrayList<String>();//数据 //遍历ls for(String item:ls){ if(item.matches("\\d+")){ s2.add(item); }else if(item.equals("(")){ s1.push(item); }else if(item.equals(")")){ //如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,并压入 s2,直到遇 到左括号为止,此时将这一对括号丢弃 while(!s1.peek().equals("(")){ s2.add(s1.pop()); } s1.pop();//消除左括号 }else{ //当item的优先级小于等于s1栈顶运算符,将 s1 栈顶的运算符弹出并压入到 s2 中,再次转到(4-1)与 s1 中新的栈顶运算符相比较; while(s1.size()!=0&&Operation.getValue(s1.peek())>=Operation.getValue(item)){ s2.add(s1.pop()); } s1.push(item); } } //将s1中剩余的运算符依次加入s2 while(s1.size()!=0){ s2.add(s1.pop()); } return s2;}
返回对应的优先级数字
class Operation{ private static int ADD=1; private static int SUB=1; private static int MUL=2; private static int DIV=2; public static int getValue(String operation){ int result=0; switch(operation){ case "+": result=ADD; break; case "-": result=SUB; break; case "*": result=MUL; break; case "/": result=DIV; break; default: break; } return result; }}
逆波兰计算器完整版
package com.atguigu.reversepolishcal;import java.util.ArrayList;import java.util.Collections;import java.util.List;import java.util.Stack;import java.util.regex.Pattern;public class ReversePolishMultiCalc { /** * 匹配 + - * / ( ) 运算符 */ static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)"; static final String LEFT = "("; static final String RIGHT = ")"; static final String ADD = "+"; static final String MINUS= "-"; static final String TIMES = "*"; static final String DIVISION = "/"; /** * 加減 + - */ static final int LEVEL_01 = 1; /** * 乘除 * / */ static final int LEVEL_02 = 2; /** * 括号 */ static final int LEVEL_HIGH = Integer.MAX_VALUE; static Stack<String> stack = new Stack<>(); static List<String> data = Collections.synchronizedList(new ArrayList<String>()); /** * 去除所有空白符 * @param s * @return */ public static String replaceAllBlank(String s ){ // \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v] return s.replaceAll("\\s+",""); } /** * 判断是不是数字 int double long float * @param s * @return */ public static boolean isNumber(String s){ Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$"); return pattern.matcher(s).matches(); } /** * 判断是不是运算符 * @param s * @return */ public static boolean isSymbol(String s){ return s.matches(SYMBOL); } /** * 匹配运算等级 * @param s * @return */ public static int calcLevel(String s){ if("+".equals(s) || "-".equals(s)){ return LEVEL_01; } else if("*".equals(s) || "/".equals(s)){ return LEVEL_02; } return LEVEL_HIGH; } /** * 匹配 * @param s * @throws Exception */ public static List<String> doMatch (String s) throws Exception{ if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty"); if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number"); s = replaceAllBlank(s); String each; int start = 0; for (int i = 0; i < s.length(); i++) { if(isSymbol(s.charAt(i)+"")){ each = s.charAt(i)+""; //栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈 if(stack.isEmpty() || LEFT.equals(each) || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){ stack.push(each); }else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){ //栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈 while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){ if(calcLevel(stack.peek()) == LEVEL_HIGH){ break; } data.add(stack.pop()); } stack.push(each); }else if(RIGHT.equals(each)){ // ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈 while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){ if(LEVEL_HIGH == calcLevel(stack.peek())){ stack.pop(); break; } data.add(stack.pop()); } } start = i ; //前一个运算符的位置 }else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){ each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1); if(isNumber(each)) { data.add(each); continue; } throw new RuntimeException("data not match number"); } } //如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列 Collections.reverse(stack); data.addAll(new ArrayList<>(stack)); System.out.println(data); return data; } /** * 算出结果 * @param list * @return */ public static Double doCalc(List<String> list){ Double d = 0d; if(list == null || list.isEmpty()){ return null; } if (list.size() == 1){ System.out.println(list); d = Double.valueOf(list.get(0)); return d; } ArrayList<String> list1 = new ArrayList<>(); for (int i = 0; i < list.size(); i++) { list1.add(list.get(i)); if(isSymbol(list.get(i))){ Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i)); list1.remove(i); list1.remove(i-1); list1.set(i-2,d1+""); list1.addAll(list.subList(i+1,list.size())); break; } } doCalc(list1); return d; } /** * 运算 * @param s1 * @param s2 * @param symbol * @return */ public static Double doTheMath(String s1,String s2,String symbol){ Double result ; switch (symbol){ case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break; case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break; case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break; case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break; default : result = null; } return result; } public static void main(String[] args) { //String math = "9+(3-1)*3+10/2"; String math = "12.8 + (2 - 3.55)*4+10/5.0"; try { doCalc(doMatch(math)); } catch (Exception e) { e.printStackTrace(); } }}
7.递归
7.1递归的概念
? 递归就是方法自己调用自己,每次调用时传入不同的值,递归有助于编程者解决复杂的问题,同时可以让代码更简洁
7.2递归调用遵循规则
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间)。
- 方法的局部变量是独立的,不会相互影响。
- 如果方法中使用的是引用类型变量(比如数组),就会共享该引用类型的数据。
- 递归必须向推出递归的条件逼近,否则就会无限递归。
- 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕
举例:
阶乘问题
//阶乘问题public static int factorial(int){ if(n==1){ return 1; }else{ return facrorial(n-1)*n; }}
8.排序算法
8.1排序算法的介绍
排序也称排序算法,排序是将一组数据,依指定的顺序进行排列的过程。
8.2排序的分类
? 1.内部排序
? 指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
? 2.外部排序
? 数据量过大,无法全部加载到内存中 ,需要借助外部存储(文件等)进行排序。
? 3.常见的排序算法分类:
?
8.3算法时间复杂度
8.3.1度量一个程序执行时间的方法
? 1.事后统计
? 这种方法可行,但有两个问题:一是要相对设计的算法的运行性能进行评测,需要实际运行该程序;二是所的时间的统计量依赖于计算机的硬件,软件等环境因素,这种方式,要在同一台计算机的相同的状态下运行,才能比较那个运行的快。(不现实)
? 2.事前估算的方法
? 通过分析某个算法的时间复杂度来判断哪个算法更优。
8.3.2时间频度
基本介绍:
? 时间频度:一个算法花费的时间与算法中语句的执行次数成正比例,那个算法中语句执行的次数多,他花费时间就多。一个算法中的语句执行的次数称为语句频度或时间频度。
8.3.3时间复杂度
? 1.一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助指针f(n),使得当n趋于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
? 2.T(n)不同,当时间复杂度可能相同。如:T(n)=n2+7n+1与T(n)=3*n2+9n 它们的时间复杂度都为O(n^2)。
? 3.计算时间复杂度的方法
- 用常数1代替运行时间中所有加法常数
- 修改后的运行次数函数中,只保留最高阶项
- 去除最高阶项的系数
8.3.4平均时间复杂度和最会时间复杂度
-
平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。 -
最坏情况下的时间复杂度称为最坏时间复杂度。一般讨论的时间复杂度均为最坏时间复杂度。这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,保证不会比此时间更长。 -
平均时间复杂度和最坏时间复杂度是否一致:
| 是否原地排序 | 是否稳定 | 最好,最坏,平均时间复杂度 | 空间复杂度 | 是否基于比较 |
---|
冒泡排序 | 是 | 是 | O(n)、 O(n^2)、 O(n^2) | O(1) | 是 | 插入排序 | 是 | 是 | O(n)、 O(n^2)、 O(n^2) | O(1) | 是 | 选择排序 | 是 | 否 | O(n^2)、 O(n2)、O(n2) | O(1) | 是 | 希尔排序 | 是 | 否 | O(n)、O(n2)、O(n1.3) | O(1) | 是 | 快速排序 | 是 | 否 | O(nlogn)、O(n^2)、O(nlogn) | O(logn)~O(n) | 是 | 归并排序 | 否 | 是 | O(nlogn)、O(nlogn)、O(nlogn) | O(n) | 是 | 计数排序 | 否 | 是 | O(n+k)、O(n+k)、O(n+k),k 是数据范围 | O(n+k) | 否 | 桶排序 | 否 | 是 | O(n)、O(n)、O(n) | O(N+M),N表示待排数据个数,M表示桶个数 | 否 | 基数排序 | 否 | 是 | O(nk)、O(nk)、O(n*k),k 是维度 | O(n+k) | 否 | 堆排序 | 是 | 否 | O(nlogn)、O(nlogn)、O(nlogn) | O(1) | 是 |
8.4算法的空间复杂度
8.4.1基本介绍
- 类似于时间复杂度的讨论,一个算法的空间复杂度定义为该算法所消耗的存储空间,它也是问题规模n的函数
- 空间复杂度是对一个算法在运行过程中临时占据存储空间的大小的量度
- 在做算法分析时,主要讨论的是时间复杂度。从用户体验上看,更看重中的是程序执行的速度。一些缓存产品和算法本质就是用空间换时间。
8.5冒泡排序
8.5.1基本排序
? 冒泡排序的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻的元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部
优化:
? 因为排序的过程中,各元素不断接近自己的位置,**如果一趟比较下来没有进行交换,就说明序列有序,**因此要在排序过程中设置一个标志flag判断元素是否进行过交换。从而较少不必要的比较。
案例:
代码:
public static void bubbleSort(int[] arr){ int temp=0;//定义变量进行交换 boolean flag=false;//表示变量,判断是否进行交换 for(int i=0;i<arr.length()-1;i++){ for(int j=0;j<arr.length()-1-i;j++){//最后i位是排序好的,不用再次排序 if(arr[j]>arr[j+1]){ flag=true; temp=arr[j]; arr[j]=arr[j+1]; arr[j+1]=temp; } } if(!flag){//在排序过程中,一次交换都没有 break; }else{//重置,进行下次判断 flag=false; } }}
8.6选择排序
8.6.1基本介绍
? 选择排序也属于内部排序法,是从与排序的数据中,按指定的规则选出某一个元素,再依据交换位置后达到排序的目的。
8.6.2选择排序思路
? 选择排序也是一种简单的排序方法。他的基本思路是:第一次从arr[0]arr[n-1]中选择最小值,与arr[0]交换,第二次从arr[1]arr[n-1]中选择最小值,与arr[1]交换,…总共通过n-1此交换,得到一个按从小到大排列有序的有序序列。
8.6.3选择排序思路分析图
代码:
public static void SelectSort(int[] arr){ for(int i=0;i<arr.length()-1;i++){ int minIndex=i; int min=arr[0]; for(int j=i+1;j<arr.length();j++){ if(min>arr[j]){ min=arr[j]; minIndex=j; } } //int min=arr[0];将最小值,放在arr[i]的位置 if(minIndex!=i){ arr[minIndex]=arr[i]; arr[i]=min; } }}
8.7插入排序
8.7.1插入排序介绍
? 插入排序属于内部排序,是对于欲排序的元素以插入的方式寻找元素的适当位置,以达到排序的目的。
8.7.2插入排序的思路
? 插入排序的基本思想:把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表只有一个元素,排序过程中,从无序表中去一个数据,与有序表元素进行比较,将他插入适当位置,使之成为新的有序表。
8.7.3插入排序思路图
代码:
public static void InsterSort(inr[] arr){ int insterVal=0;//元素数据 int insterIndex=0;//元素待插入下标 for(int i=1;i<arr.length();i++){ //定义待插入的数 insterVal=arr[i]; insterIndex=i-1;//即arr[i]的前面的元素的序列 //给insterVal找插入位置 //insterIndex>=0保证不越界; //insterVal<arr[insterIndex],还没有找到位置 while(insterIndex>=0&&insterVal<arr[insterIndex]){ arr[insterIndex+1]=arr[insterIndex]; //后移 insterIndex--; } //当推出while循环,说明插入的位置已经找到,insterIndex+1; if(insterIndex+1!=i){ arr[insterIndex+1]=insterVal; } }}
8.8希尔排序
当需要插入的数是较小的数时,后移的次数明显增多,对效率有影响
8.8.1希尔排序介绍
? 希尔排序也是一种插入算法,他是简单插入排序算法经过改进的版本,也成为缩小增量排序。
8.8.2希尔排序的基本思想
? 希尔排序是把记录按下标的一定增量分组,对每株使用直接插入排序;随着增量逐渐减少,每组包含的关键字越来越多,当增量减至1时,整个文件恰被分为1组,算法便终止
8.8.3希尔排序示意图
代码:
//交换法public static void shellSort(int[] arr){ int temp=0; for(int gap=arr.length()/2;gap>0;gap/=2){ for(int i=gap;i<arr.length();i++){ //遍历各组中所有元素(共gap组),步长为gap for(int j=i-gap;j>=0;j-=gap){ //如果当前元素大于加上步长的元素,说明交换 if(arr[j]>arr[j+gap]){ temp=arr[j]; arr[j]=arr[j+gap]; arr[j+gap]=temp; } } } }}
//移位法public static void shellSort(int[] arr){ //增量gap,并逐步缩小增量 for(int gap=arr.length()/2;gap>0;gap/=2){ for(int i=gap;i<arr.length();i++){ int j=i; int temp=arr[j]; if(arr[j]<arr[j-gap]){ while(j-gap>=0&&temp<arr[j-gap]){ //移动 arr[j]=arr[j-gap]; j-=gap; } arr[j]=temp; } } }}
8.9快速排序
8.9.1快速排序介绍
? 快速排序是对冒泡排序的一种改进。基本思路:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的数据要小,然后再按此方法对两部分数据分别进行快速排序,整个排序可以递归进行,以此达到整个数据变成有序序列
8.9.2快速排序示意图
代码:
public static void QuickSort(int[] arr,int left,int right){ int l=left;//做下标 int r=right;//右下标 int pivot=arr[(right+left)/2]; int temp=0;//临时变量,作为交换使用 //while循环的目的是让比pivot值小的放到左边 while(l<r){ while(arr[l]<pivot){ l+=1; } while(arr[r]>pivot){ r-=1; } //如果l=r;说明两边之相等 if(l>=r){ break; } //交换 temp=arr[l]; arr[l]=arr[j]; arr[j]=temp; //如果交换完后发现arr[l]=pivot. if(arr[l]==pivot){ r-=1; } //如果交换完后发现arr[r]=pivot. if(arr[r]==pivot){ l+=1; } //如果l=r;必须l++;r-- if(l==r){ l+=1; r-=1; } //向左递归 if(left<r){ QuickSort(arr,left,r); } //向右递归 if(right>l){ QuickSort(arr,l,right); } }}
8.10归并排序
8.10.1归并排序介绍
? 归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略
8.10.2归并排序思路图
代码:
//分+合public static void mergetSort(int[] arr,int left,int right,int[] temp){ if(left<right){ int mid=(left+right)/2; mergetSort(arr,left,mid,temp); mergetSort(arr,mid+1,right,temp); merge(arr,left,mid,right,temp) }}//合并方法public atatic void merge(int[] arr,int left,int mid,int right,int[] temp){ int i=left;//左侧有序序列初始索引 int j=mid+1;//右侧有序序列初始索引 int t=0;//指向temp数组的当前索引 /* 一。 先把左右两边的有序序列写入temp 知道有一边处理完毕 */ while(i<=mid&&j<=right){ // if(arr[i]<=arr[j]){//左小于右 temp[t]=arr[i]; t+=1; i+=1; }else{ temp[t]=arr[j]; t+=1; j+=1; } } /* 二 把剩余一侧全部加入 */ while(i<=mid){//左侧剩余 temp[t]=arr[i]; t+=1; i+=1; } while(j<=right){//右侧剩余 temp[t]=arr[j]; t+=1; j+=1; } /* 三 将temp数组导入arr数组 */ t=0; int tempLeft=left; while(tempLeft<=right){ arr[tempLeft]=temp[t]; t+=1; tempLeft+=1; }}
8.11基数排序
8.11.1基数排序介绍
- 基数排序属于“分配时排序”,又称“桶子法”。通过键值的各个位的值,将要排序的元素分配至某个桶
- 基数排序属于稳定性排序,基数排序是效率搞得稳定性排序
- 基数排序是桶排序的扩展
8.11.2基数排序基本思路
? 将所有待排序的数值统一为同样长度的长度,短着前部补零,从低位开始,依次排序。
代码:
public static void radixSort(int[] arr){ int max=arr[0]; //遍历寻找最大值 for(int i=1;i<arr.length();i++){ if(arr[i]>max){ max=arr[i]; } } //判断最大数有几位 int maxLength=(max+"").length(); //定义一份二维数组,表示10个桶 //为防止存值时数据溢出,每个一维数组大小设为arr.length int[][] bucket=new int[10][arr.length]; //定义一个一维数组表示每个桶中存入数据的个数 int[] bucketElementCounts=new int[10]; for(int i=0,n=1;i<maxLength;i++,n*=10){ for(int j=0;j<arr.length;j++){ //取出每位数的对应位的值 int digitOfElement=arr[j]/n%10; //放入到对应的桶中 bucket[digitOfElement][bucketElementCounts[digitOfElement]]=arr[j]; bucketElementCounts[digitOfElement]++;//防止某位数多次出现 } //按照这个通的顺序(一维数组的下标依次取出,放入原数组) int index=0; //遍历每个桶, for(int k=0;k<bucketElementCounts.length;k++){ //判断筒不为空 if(bucketElementCounts[k]!=0){ for(int l=0;l<bucketElementCounts[k];l++){ arr[index++]=bucket[k][l]; } } //清零,方便下次运行 bucketElementCounts[k]=0; } }}
8.12堆排序
8.12.1堆排序的基本介绍
- 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,他的最坏,最好,平均时间复杂度均为O(nlogn),他也是不稳定排序
- 堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子的值,称为大顶堆,注意:没有要求节点左孩子与有孩子的关系
- 每个结点的值都小于其左右孩子的值,称为小顶堆。
8.12.2堆排序的思想
- 将待排序的序列构造成一个大顶堆
- 此时,序列最大的值就是根节点
- 将其与末尾元素交换
- 然后将n-1个元素重新构造成一个大顶推,反复执行
代码:
//将一个数组转为大顶堆public void adjustHeap(int[] arr,int i,int length){ int temp=arr[i];//取出当前元素,保存临时变量 //2*i+1表示当前节点的左子节点 for(int k=i*2+1;k<length;k=k*2+1){ if(k+1<length&&arr[k]<arr[k+1]){//左子节点值小于右子节点 k++;//k指向右子节点 } if(arr[k]>temp){ arr[i]=arr[k];//当前较大值付给当前节点 i=k;//继续循环 }else{ break; } } arr[i]=temp;}public void heapSort(int[] arr){ //构建大顶堆 for(int i=arr.length/2-1;i>=0;i--){ adjustHeap(arr,i,arr.length); } //将其与末尾元素交换 int temp=0; for(int j=arr.length-1;j>0;j--){ temp=arr[j]; arr[j]=arr[0]; arr[0]=temp; adjustHeap(arr,0,j); }}
9.查找算法
9.1查找算法介绍
在Java中,我们常用的查找方法有四种:
- 顺序查找
- 二分查找法/折半查找
- 插值查找
- 斐波那契查找
9.2顺序查找
思路分析:
? 按照顺序依次遍历查找,进行比对。
代码:
public static void seqSearch(int[] arr,int value){ //遍历对比 for(int i=0;i<arr.length;i++){ if(arr[i]==value){ return i; } } return -1;}
9.3二分查找法
思路分析:
- 首先确定数组的中间位置,mid=(left+right)/2;
- 然后让findVal与arr[mid]比较
- findVal>arr[mid],向右递归查找
- indVal<arr[mid],向左递归查找
- indVal=arr[mid],返回
- 结束递归
- 找到就结束递归
- 递归遍历完,仍未找到,即当left>right,就退出递归
代码:
public static void binarySearch(int[] arr,int left,int right,int findVal){ //当left>right仍未找到 if(left>right){ return -1; } //寻找数组中间值 int mid=(left+right)/2; int midVal=arr[mid]; if(findVal>midVal){ //向右递归 return binarySearch(arr,mid+1,right,findVal); }else if(findVal<mindVal){ //向左递归 return binarySearch(arr,left,mid,findVal); }else{ return mid; }}
9.4插值查找
思路分析:
-
插值查找原理 ? 插值查找类似于二分法查找,不同的是插值查找每次从自适应的mid处看是查找 -
折半查找中的索引mid=(left+right)/2; -
插值查找的索引mid=(right-left)/(arr[right]-arr[left])*(findVal-arr[left])+left
代码:
public static void insterSearch(int[] arr,int left,int right,int findVal){ //确保mid不会越界 if(left>right||findVal<arr[0]||findVal>arr[arr.length-1]){ return -1; } //求mid int mid=left+(right-left)/(arr[right]-arr[left])*(findVal-arr[left]); if(findVal>midVal){ //向右递归 return binarySearch(arr,mid+1,right,findVal); }else if(findVal<mindVal){ //向左递归 return binarySearch(arr,left,mid,findVal); }else{ return mid; }}
注意事项:
- 对于数据量较大,关键字分布比较均匀的查找表来说,采用插值查找,速度较快。
- 关键字分布不均匀的情况下,该方法不一定比折半查找好
9.5斐波那契查找
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”。
斐波那契数列指的是这样一个数列: 0,1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711…… 它的规律是:这个数列从第 3 项开始,每一项都等于前两项之和。 在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*),显然,斐波那契数列是一个线性递推数列。
思路分析:
- 把有序数组的长度扩充到 nums. length = F[k] - 1,k 是满足条件的最小值,比如数组长度为15,那么就把它长度扩充到 F[6] - 1 = 20,所有在末尾添加的扩充元素都是原数组最后一个元素的复制。
- 找到 mid 元素,不断进行二分比较,直到找到目标元素为止,这一步的做法与折半查找很相似,仅仅是计算 mid 的公式从 (low + high) / 2改为 low + ( F[k-1] - 1)。
对f(k-1)-1的理解
为什么需要把数组长度扩充到 F[k] - 1 而不是 F[k] 或者 F[k+1]? 其实这是为了能正确递归计算mid值,看上图可发现 F[k]-1 = (F[k-1] + F[k-2]) - 1 = (F[k-1]-1) + 1 + (F[k-2]-1),中间的1就是我们二分的锚点mid,如果目标在左区,数组长度就缩到(F[k-1]-1),如果在右区,数组长度就缩到(F[k-2]-1),否则就等于mid完成查找。而(F[k-1]-1)又能拆成(F[k-2]-1)+1+(F[k-3]-1),这样递归分割下去就能不断的缩小区间直至找到目标。
代码:
//编写斐波那契数列public static int[] fib(){ int[] f=new int[maxsize]; f[0]=1; f[1]=1; for(int i=2;i<maxsize;i++){ f[i]=f[i-1]+f[i-2]; } return f;}//编写斐波那契算法public static void fibonacciSearch(int[] arr,int findVal){ int low=0; int high=a.length-1; int k=0;//表示斐波那契数值下标 int mid=0; int f[]=fib();//获取斐波那契数列 while(high>f[k]-1){//获取斐波那契数列分割值的下标 k++; } int[] temp=Arrays.copyOf(arr,f[k]); //使用arr数组最后的数补充temp数组位数 for(int i=high+1;i<temp.length;i++){ temp[i]=arr[high]; } while(low <= high) { mid = low + f[k-1]-1; if(findVal < tempr[mid]) { high = mid - 1; k -= 1; }else if(findVal > tempr[mid]) { low = mid + 1; k -= 2; }else { if(mid < temp.length) { return mid; }else { return temp.length - 1; } } } return -1;}
10.树结构的基础知识
10.1二叉树
10.1.1为什么需要树这种数据结构
1.数组存储方式的分析
优点:
通过下标方式访问元素,速度快。对于有序数组,还可以使用二分查找提高检索速度
缺点:
如果要检索具体某个值,或者插入(按一定顺序)会整体移动,效率较低
2.链式存储方式的分析
优点:
在一定程度上对数组存储方式有优化(比如:插入一个数值节点,只需要将插入节点,链表到链表中即可,删除效率也很好)。
缺点:
在进行检索时,效率仍然较低,都需要从头开始遍历。
3.树存储方式的分析
能提高数据存储,读取的效率。
10.1.2树示意图
结点的度:一个结点的子树的个数记为该结点的度;如A结点的度为4,B结点的度为3
树的度:结点树的度数最大的值为树的度,如上图的树的度为3,因为B结点的度为3,是这颗树最大的结点度
叶子结点:没有下一级相关联的结点为叶子结点,如E,F,C,G
分支结点:非叶子结点为分支结点,包括根结点
内部结点:除根结点外,分支结点为内部结点
10.1.3二叉树的概念
1.树有很多种,每个子结点最多只能有两个节点的一种形式的二叉树
2.二叉树有的子节点分为左节点和右节点
3.如果该二叉树的所有叶子节点都在最后一层,并且节点总数=2^n-1,n为层数,则我们称为满二叉树。
4.如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们成为完全二叉树
10.1.4二叉树遍历说明
? 使用前序,中序,后序对二叉树进行遍历
- 前序遍历:先输出父节点,在遍历左子树和右子树
- 中序遍历:先遍历左子树,在输出父节点,在遍历右子树
- 后序遍历:先遍历左子树和右子树,在输出父节点
定义节点:
class HeroNode { private int no; private String name; private HeroNode left; //默认null private HeroNode right; //默认null public HeroNode(int no, String name) { this.no = no; this.name = name; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } public HeroNode getLeft() { return left; } public void setLeft(HeroNode left) { this.left = left; } public HeroNode getRight() { return right; } public void setRight(HeroNode right) { this.right = right; } @Override public String toString() { return "HeroNode [no=" + no + ", name=" + name + "]"; }
前序遍历代码:
public static void preOrder(){ System.out.println(this); if(this.left!=null){ this.left.preOrder(); } if(this.right!=null){ this.right.preOrder(); }}
中序遍历代码:
public static void infixOrder(){ if(this.left!=null){ this.left.infixOrder(); } System.out.println(this); if(this.right!=null){ this.right.infixOrder(); }}
后序遍历代码:
public static void postOrder(){ if(this.left!=null){ this.left.postOrder(); } if(this.right!=null){ this.right.postOrder(); } System.out.println(this);}
10.1.5二叉树-查找指定节点
? 使用前序,中序,后序来查询指定的节点
前序查找思路:
- 先判断当前节点是不是要查询的
- 如果相等则返回
- 如果不等,则判断当前节点的左子节点是否为空,如果不为空,则递归查询
- 如果左递归查询找到节点就返回,否则继续判断,当前节点的右子节点是否为空,如果不为空,则继续递归查找
中序查找思路:
- 判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
- 如果找到,则返回,如果没有找到,就和当前节点比较,如果是就返回,否则继续右子节点递归
- 如果右递归中序查找,找到就返回,否则返回null
后序查找思路:
-
判断当前节点的左子节点是否为空,如果不为空,则递归中序查找 -
如果找到,则返回,如果没有找到,就和当前节点的右子节点进行比较,如果不为空且未找到就递归 -
和当前节点进行比较,找到就返回,否则返回null
前序遍历查找代码
public HeroNode preOrderSearch(int no){ System.out.println("前序遍历"); if(this.no==no){//比较当前节点 return this; } //判断当前节点的左子节点是否为空,不为空就继续递归 HeroNode resNode=null; if(this.left!=null){ resNode=this.left.preOrderSearch(no); } //如果不为空,则说明找到了 if(resNode!=null){ return resNode; } //向右递归 if(this.right!=null){ resNode=this.right.preOrderSearch(no); } return resNode;}
中序遍历查找代码
public HeroNode infixOrderSearch(int no){ System.out.println("中序遍历"); //判断当前节点的左子节点是否为空,不为空就继续递归 HeroNode resNode=null; if(this.left!=null){ resNode=this.left.infixOrderSearch(no); } //如果不为空,则说明找到了 if(resNode!=null){ return resNode; } if(this.no==no){//比较当前节点 return this; } //向右递归 if(this.right!=null){ resNode=this.right.infixOrderSearch(no); } return resNode; }
后序遍历查找代码
public HeroNode postOrderSearch(int no){ System.out.println("中序遍历"); //判断当前节点的左子节点是否为空,不为空就继续递归 HeroNode resNode=null; if(this.left!=null){ resNode=this.left.postOrderSearch(no); } //如果不为空,则说明找到了 if(resNode!=null){ return resNode; } //向右递归 if(this.right!=null){ resNode=this.right.postOrderSearch(no); } if(resNode!=null){ return resNode; } if(this.no==no){//比较当前节点 return this; } return resNode;}
10.1.6二叉树-删除节点
要求:
- 如果删除的节点是叶子节点,就删除该节点
- 如果要删除的节点是非叶子节点,则删除该子树
思路:
- 如果该树只有一个root节点,就将二叉树置空
- 因为二叉树是单向的,所以需要判断当前节点的子节点是不是需要删除的节点
- 如果当前节点的左子节点不为空,并且左子节点就是需要删除的节点,则this.left=null;
- 如果当前节点的右子节点不为空,并且右子节点就是需要删除的节点,则this.right=null;
- 如果2&3步没有删除节点,就需要将左子树递归删除
- 如果第4步也没有删除节点,则应当向右递归删除
代码:
public void delNode(int no){ if(this.left!=null&&this.left.no==no){ this.left=null; return; } if(this.right!=null&&this.right.no==no){ this.right=null; return; } //删树 if(this.left!=null){ this.left.delNode(no); } if(this.right!=null){ this.right.delNode(no); } }
10.2顺序存储二叉树
10.2.21顺序存储二叉树的概念
基本说明:
从数据存储来看,数据存储方式和树的存储方式可以相互转换,即数组可以转换成树,书也可以转换成数组。
顺序存储二叉树的得点:
- 顺序二叉树通常只会考虑完全二叉树
- 第n个元素的左子节点为2*n+1
- 第n个元素的右子节点为2*n+2
- 第n个元素的父节点为(n+1)/2
代码:
int[] arr;public void preOrder(int index){ if(arr==null||arr.length==0){ System.out.println("数组为空") } System.out.println(arr[index]); if((index*2+1)<arr.length){ preOrder(index*2+1); } if((index*2+2)<arr.length){ preOrder(index*2+2); }}
10.3线索化二叉树
10.3.1线索二叉树基本介绍
- n个节点的二叉链表中含有n+1【公式2n-(n-1)=n+1】个空指针域。利用二叉链表的空指针域,存放指向该节点在某种遍历次序下的前驱和后记节点的指针(这种附加的指针称为线索)
- 这种加上线索的二叉树链表又称为线索链表,相应的二叉树称为线索二叉树。根据线索性质的不同,线索二叉树可分为前序线索二叉树,中序…,后序…
- 一个节点的前一个节点,称为前驱节点
- 一个结点的后一个节点,称为后继节点
思路分析:
- left指向左子树,也可能指向前驱节点
- right指向右子树,也可能指向后继节点
代码:
定义节点:
class HeroNode { private int no; private String name; private HeroNode left; //默认null private HeroNode right; //默认null //如果为0,表示左右子树;如果为1,表示前驱后继节点 private int leftType; private int rightType; public HeroNode(int no, String name) { this.no = no; this.name = name; } public int getNo() { return no; } public void setNo(int no) { this.no = no; } public String getName() { return name; } public void setName(String name) { this.name = name; } public HeroNode getLeft() { return left; } public void setLeft(HeroNode left) { this.left = left; } public HeroNode getRight() { return right; } public void setRight(HeroNode right) { this.right = right; } @Override public String toString() { return "HeroNode [no=" + no + ", name=" + name + "]"; }
实现线索化二叉树
class ThreadedBinaryTree{ private HeroNode root; //为实现线索化,需要创建一个指向当前节点的前驱节点的指针 //在递归进行线索化时,pre总是被保留前一个节点 private HeroNode pre=null; public void setRoot(HeroNode root){ this.root=root; } //实现线索化二叉树 public void threadedNodes(HeroNode node){//node就是当前需要线索化的节点 if(node==null){ return; } //先线索化左子树 threadedNodes(node.getLeft()); //线索化当前节点 //处理前驱节点 if(node.getLeft()==null){ //让当前节点的做指针指向前驱节点 node.setLeft(pre); //修改类型 node.setLeftType(1); } //处理后继节点 if(pre!=null&&pre.getRight()==null){ pre.setRight(node); pre.setRightType(1); } pre=node; //线索化右子树 threadedNodes(node.getRight()); }}
遍历线索二叉树
//遍历中序线索化二叉树 public void threadeList(){ //定义一个变量,存储当前节点,从root开始 HeroNode node=root; while(node!=null){ //循环找到leftType==1节点 while(node.getLeftType()==0){ node=node.getLeft(); } //打印当前节点 System.out.println(node); while(node.getRightType()==1){ //如果当前节点的右指针指向后继节点,就一直输出 node=node.getRight(); System.out.println(node); } //替换遍历节点 node=node.getRight(); } }
删除节点思路:
- 因为二叉树是单向的,所以我们是判断当前节点的子节点是否需要删除节点,而不能去判断这个节点是不是需要删除节点
- 如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,就将this.left=null;并且就返回(结束帝都删除)
- 如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,就将this.right=null;并且就返回(结束帝都删除)
- 否则递归删除
代码:
public void delNode(int no){ if(this.left!=null&&this.left.no==no){ this.left=null; return; } if(this.right!=null&&this.right.no==no){ this.right=null; return; } //递归删除 if(this.left!=null){ this.left.delNode(no); } if(this.right!=null){ this.right.delNode(no); }}
10.4赫夫曼树
10.4.1基本介绍
- 给定n个权值作为n个叶子结点,构造一颗二叉树,若该树的带权路径长度达到最小(wpl),成这样的二叉树为最优二叉树,也称为哈弗满树
- 赫夫曼树是带权路径最短的树,权值较大的节点离根节点近
10.4.2赫夫曼树几个重要概念和举例说明
- 路径和路径长度: 在一棵树中,从一个节点往下一个节点可以达到的孩子或孙子节点之间的通路,成为路径。通路中分支的数目成为路径长度。 若规定根结点的层数为1,则从根节点到第L层节点的路径长度为(L-1)
- 节点的权和带权路径长度: 若将书中节点付给一个有着某种含义的数值,则这个数值称为该点的权。节点的带权路径长度为: 从根节点到该节点之间的路径长度与该节点的权的乘积
- 树的带权路径长度: 树的带权路径长度规定为所有叶子结点的带权路径长度之和,
10.4.3构成赫夫曼树的步骤
给你一列数列{13,7,8,3,29,6,1},要求转成一颗赫夫曼树
- 从小到大进行排序 ,将每一个数据,每个数据都是一个节点,每个节点可以看作是一颗最简单的二叉树
- 取出根节点权值最小的两棵二叉树
- 组成一颗新的二叉树,该二叉树的根节点的权值是前面两个二叉树根节点权值的和
- 再将这颗新的二叉树,以根节点权值的权值大小再次排序,不断重复,
在java中接口comparable使我们经常要接触到的,比如对集合或者数组进行排序,我们经常使用到Arrays.sort()或者Collections.sort().当集合中的对象是自定义的对象时,我们有两种方法能够使排序方法应用到自定义对象的集合(数组)中。
代码:
//创建节点类 //为了让Node对象持续排序Collection集合排序 //让Node实现Comparable接口 class Node implements Comparable<Node>{ int value;//节点权值 Node left;//只想左子节点 Node right;//指向右子节点 public Node(int value){ this.value=value; } @Override public String toString(){ return "NOde[value="+value+"]"; } @Override public int compareTo(Node o){ //表示从小到大排序 return this.value-o.value; } }
//编写实现赫夫曼属的方法 public static Node creaHuffmanTree(int[] arr){ /* 1.遍历arr数组 2.将arr数组中每个元素构成一个Node 3.将Node放入到ArrayList中 */ List<Node> nodes=new ArrayList<Node>(); for(int value: arr){ node.add(new Node(value)); } //循环 while(nodes.size()>1){ //排序 Collection.sort(nodes); System.out.println("nodes="+nodes); /* 去除两个权值最小的二叉树 即arr[0]和arr[1]; */ Node leftNode=nodes.get(0); Node rightNode=nodes.get(1); //构建新的二叉树 Node parent=new Node(leftNode.value+rightNode.value); parent.left=leftNode; parent.right=rightNode; //从Array List数组中删除处理过的 nodes.remove(leftNode); nodes.remove(rightNode); //将parent添加到Array List中 nodes.add(parent); } //返回赫夫曼书的root节点 return nodes.get(0); }
//前序遍历 public void preOrder(){ System.out.println(this); if(this.left!=null){ thix.left.preOrser(); } if(this.right!=null){ this.right.preOrder(); } }
10.5二叉排序树(BST)
案例需求:
给你一个数列{7,3,10,12,5,1,9},要求能够高效地完成对数据的查询和添加
案例分析:
- 使用数组
- 数组未排序:,优点:直接在数组尾部添加,速度快。缺点:查询速度慢
- 数组排序:,优点:可以使用二分查找法,查找速度快。缺点:为了保证数组有序,再添加新数据时,找到插入位置后,后面的数据需要整体移动,速度慢。
- 使用链式存储-链表
- 不管链表是否有序,查询速度都慢,添加速度比数组快,不需要整体移动。
- 使用二叉排序树
10.5.1二叉排序树介绍
二叉排序树:BST,对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右节点的值比当前节点的值大
**特别说明:**如果有相同的值,可以将该节点放在左子节点或右子节点
10.5.2二叉排序树的创建
一个数组创建成对应的二叉排序树,并使用中序遍历二叉树,比如:数组为Array{7,3,10,13,5,1,9},创建成对应的二查排序树为:
代码:
public void add(Node node){ if(node==null){ return; } //如果二叉树为空,则直接给root赋值 if(root==null){ roor=node; }else{ root.add(node); } if(node.value<this.value){ //如果做节点为空 if(this.left==null){ this.left=node; }else{ //如果不为空,则递归添加 this.left.add(node); } }else{ if(this.right==null){ this.right=node; }else{ this.right.add(node); } }}
10.5.3二叉排序树的删除
二叉排序树的删除情况比较复杂,有三种情况:
- 删除叶子节点
- 删除只有一棵子树的节点
- 删除有两棵子树的节点
思路分析:
第一种情况—删除叶子节点:
- 需要先找到要删除的节点targetNode
- 找到targetNode的父节点parent
- 确定targetNode是左孩子还是右孩子
- 左子节点:parent.left=null;右子节点:parent.right=null;
第二种情况—删除只有一棵子树的节点
- 需要先找到要删除的节点targetNode
- 找到targetNode的父节点parent
- 确定targetNode的子节点是左子节点还是右子节点
- 确定targetNode是parent的左孩子还是右孩子
- 如果targetNode有左子节点
- 如果targetNode是parent的左子节点:parent.left=targetNode.left
- 如果targetNode是parent的右子节点:parent.right=targetNode.left
- 如果targetNode有右子节点
- 如果targetNode是parent的左子节点:parent.left=targetNode.right
- 如果targetNode是parent的右子节点:parent.right=targetNode.right
第三种情况—删除有两棵子树的节点
- 需要先找到要删除的节点targetNode
- 找到targetNode的父节点parent
- 从targetNode的右子树中找到最小的节点
- 用一个临时变量,将最小结点的值保存到temp
- 删除该最小节点
- targetNode.value=temp;
代码:
//查询要删除的节点public Node search(int value){ if(root==null){ return null; }else{ return root.search(value); }}
//查找父节点public Node searchParent(int value){ if(root==null){ return null; }else{ return root.searchParent(value); }}
//删除node为根节点的二叉排序树的最小节点public int delRightTreeMin(Node node){ Node target=node; //循环的查找 while(target.left!=null){ target=target.left; } delNode(target.value); return target.value;}
//删除节点public void delNode(int value){ if(root==null){ return; }else{ //找到要删除的节点targetNode节点 Node targetNode=search(value); //如果没有找到 if(targetNode==null){ return; } //找targetNode的父节点 Node parent=searchParent(value); //如果要删除的节点是叶子节点 if(targetNode.left==null&&targetNode.right==null){ //判断targetNode是parent的左右节点 if(parent.left!=null&&parent.left.value==value){ //是左节点 parent.left=null; }else if(parent.right!=null&&parent.right.value==value){ parent.right=null; } }else if(targetNode.left!=null&&targetNode.right!=null){ //删除有两棵子树的节点 //寻找targetNode为根节点右子树的最小值 int minval=delRightTreeMin(targetNode.right); targetNode.value=mnval; }else{ //删除只有一棵子树的节点 if(targetNode.left!=null){//删除的节点有左子节点 if(parent!=null){ if(parent.left.value==value){//如果targetNode是parent的左子节点 parent.left=targetNode.left; }else{ parent.right=targetNode.left; }else{ root=targetNode.left;//递归 } } }else{//删除的节点有右子节点 if(parent!=null){ if(parent.right.value==value){ parent.right=targetNode.right; }else{ parent.left=targetNode.right; }else{ root=targetNode.right; } } } } }}
10.6平衡二叉树(AVL)
给你一个数列{1,2,3,4,5,6},要求创建一颗二叉排序树,并分析问题所在
左边BST存在的问题分析:
- 左子树全部为空,从形式上看,更像一个单链表
- 插入速度没有影响
- 查询速度明显下降(因为要一次比较),不能发挥BST的优势,因为每次要比较左子树,其查询速度比单链表还慢
- 解决方案-平衡二叉树(AVL)
10.6.1基本介绍
- 平衡二叉树也叫二叉搜索树,可以保证查询效率最高
- 具有以下特点:它是一棵空树或它的左右两棵子树的高度差的绝对值不超过1,并且两棵子树都是一颗平衡二叉树。
返回以该节点为根节点的树的高度
public int height(){ return Math.max(left==null?0:left.height(),right==null?0:right.height())+1;}
10.6.2单旋转(左旋)
思路分析:
- 创建一颗新节点newNode,值等于当前结点的值
- 把新节点的左子树设成当前节点的左子树
- 把新结点的右子树设成当前节点的右子树的左子树
- 把当前节点的值换成右子节点的值
- 把当前节点的右子树设置成为右子树的右子树
- 把当前节点的左子树设置为新节点
代码:
private void leftRotate(){ //创建一个新结点,以当前根结点的值 Node newNode=new Node(value); //把新节点的左子树设成当前节点的左子树 newNode.left=left; 把新结点的右子树设成当前节点的右子树的左子树 newNode.right=right.left; //把当前节点的值换成右子节点的值 value=right.value; //把当前节点的右子树设置成为右子树的右子树 right=right.right; // left=newNode;}
10.6.3单旋转(右旋转)
思路分析:
- 创建一个新节点newNode,值等于当前结点的值
- 把新节点的右子节点指向当前节点的右子树
- 把当前节点的左子节点指向当前节点左子树的右子树
- 把当前结点的值设置为左子结点的值
- 把当前节点的左子树设置成为左子树的左子树
- 把当前结点的右子树设置为新节点
代码:
private void rightrotate(){ Node newNode=new Node(value); newNode.right=right; newNode.left=left.right; value=left.value; left=left.left; right=newNode;}
10.6.4双旋转
思路分析:
- 当符号右旋转的条件时
- 如果他的左子树的右子树高度大于它的左子树的高度
- 先对当前节点的左节点进行左旋转
- 在对当前节点进行右旋转
代码:
// 添加结点的方法 // 递归的形式添加结点,注意需要满足二叉排序树的要求 public void add(Node node) { if (node == null) { return; } // 判断传入的结点的值,和当前子树的根结点的值关系 if (node.value < this.value) { // 如果当前结点左子结点为null if (this.left == null) { this.left = node; } else { // 递归的向左子树添加 this.left.add(node); } } else { // 添加的结点的值大于 当前结点的值 if (this.right == null) { this.right = node; } else { // 递归的向右子树添加 this.right.add(node); } } //当添加完一个结点后,如果: (右子树的高度-左子树的高度) > 1 , 左旋转 if(rightHeight() - leftHeight() > 1) { //如果它的右子树的左子树的高度大于它的右子树的右子树的高度 if(right != null && right.leftHeight() > right.rightHeight()) { //先对右子结点进行右旋转 right.rightRotate(); //然后在对当前结点进行左旋转 leftRotate(); //左旋转.. } else { //直接进行左旋转即可 leftRotate(); } return ; //必须要!!! } //当添加完一个结点后,如果 (左子树的高度 - 右子树的高度) > 1, 右旋转 if(leftHeight() - rightHeight() > 1) { //如果它的左子树的右子树高度大于它的左子树的高度 if(left != null && left.rightHeight() > left.leftHeight()) { //先对当前结点的左结点(左子树)->左旋转 left.leftRotate(); //再对当前结点进行右旋转 rightRotate(); } else { //直接进行右旋转即可 rightRotate(); } } }
11.多路查找树
11.1二叉树与B树
11.1.1二叉树的问题分析
二叉树的操作效率高,但也存在问题:
二叉树需要加载到内存,如果二叉树的节点少,没有问题,如果二叉树的节点多,就会出现:
- 问题1:在构建二叉树时,需要多次i/o操作,速度又影响
- 问题2:节点多,会造成二叉树的高度很大,会降低操作速度
11.1.2多叉树
- 在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树
- 多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。
11.1.3B树的基本介绍
B树通过重新组织节点,降低树的高度,并减少i/o读写次数来提升效率
- 如图B树通过重新组织节点,降低树的高度
- 文件系统及数据库系统的设计者利用磁盘预读原理,将每个结点的大小设置成一页,这样每个节点只需要一次I/O就可以完全载入
11.2 2-3树
11.2.1 2-3树是最简单的B树结构,具有以下特点:
- 2-3树的所有叶子节点都在同一层(只要是B树均满足)
- 有两个节点的节点焦耳节点,二节点要么没有节点,要么有两个节点
11.2.2 2-3树的应用案例
将数列{16,24.12,32,14,26,34,10,8,28,38,20}构建成2-3树,并保证数据插入的顺序大小。
11.3 B树,B+树,B*树
11.3.1B树的介绍
B-tree树即B树,又称B-树
对上图的说明:
- B树的阶:节点的最多子结点的个数。
- B-树的搜索从根节点开始,对节点内的关键字进行二分查找,如果命中则结束,否则进入关键字所属范围的儿子节点
- 关键字集合分布在整片树中
- 搜索有可能在非叶子节点结束
11.3.2B+树的介绍
B+树是B树的变体,也是一种多路搜索树
对上图的说明:
- B+树的搜索与B树也基本相同,区别是B+树只有达到叶子节点才命中
- 所有关键字都出现在叶子节点的链表中(及数据只能在叶子节点【也叫稠密索引】),且链表中的关键字恰好是有序的
- 不吭在非叶子节点命中
- 非叶子节点相当于是叶子节点的索引(稀疏索引),叶子节点相当于是存储数据的数据层
- 更适合文件索引系统
- B树与B+树各有自己的应用场景
11.3.3B*树的介绍
B*树是B+树的变体,在B+树的非跟和非叶子节点再增加指向兄弟的指针
对上图的说明:
- B*树定义了非叶子节点关键字个数至少为(2/3)*M,即块的最低使用率为2/3.而B+树的块的最低使用率为1/2
- B*树分配新节点的概率比B+树要低,空间使用率更高
12. 图
12.1图基本介绍
12.1.1为什么要有图
- 线性表局限于一个直接前驱和一个直接后继的关系
- 树也只能有一个直接前驱也就是父节点
- 当我们需要表示多对多的关系时,这里我们就用到了图
12.1.2图的举例说明
图是一种数据结构,其中节点可以具有零个或多个相邻元素。两个相邻节点之间的连接称为边
12.1.3图的常用概念
-
顶点 -
边 -
路径 -
无向图 -
有向图 -
带权图
12.2图的表达方式
图的表达方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)
12.2.1邻接矩阵
邻接矩阵是表示图形中定顶点之间相邻关系的矩阵
12.2.2邻接表
- 邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边是不存在的,会造成空间的一定损失
- 邻接表的实现只关心存在的边,不关心不存在的边。没有空间浪费,邻接表由数组+链表组成
12.3图的快速入门
-
要求: -
思路分析:
- 存储顶点 String 使用ArrayList
- 保存矩阵 int[][] edges
代码:
12.4图的深度优先遍历基本算法
12.4.1图遍历介绍
及对每个结点的访问。一般有两种访问策略:
- 深度优先算法
- 广度优先算法
图的定义代码:
public class Graph{ private ArrayList<String> vertexList;//存储顶点集合 private int[][] edges;//存储图对应的邻接矩阵 private int numOfEdges;//表示边的数目 private boolean[] isVisited;// 定义数组boolean[]。记录某个节点是否被访问 }
12.4.2深度优先遍历思想
图的深度优先算法:
- 图的深度优先遍历,从初始访问节点出发,初始访问节点可能有多个邻接节点,深度优先遍历的策略就是首先访问第一个邻接节点,然后在以这个被访问的临界节点作为初始节点,访问它的第一个邻接节点,
- 这样的访问策略是优先往纵向挖掘深入,,而不是对一个结点的所有邻接节点进行横向访问
- 深度优先搜索是一个递归的过程
12.4.3深度优先遍历算法步骤
-
访问初始节点v,并标记节点v为已访问。 -
查找节点v的第一个邻接节点w -
若w存在,则继续执行4,否则返回到第一步,将从v的下一个节点出发 -
若w未被访问,对w进行深度优先遍历递归(即把w当做另一个v,然后进行步骤123) -
查找节点v的w邻接节点,转到步骤3 -
分析图
12.5图的广度优先遍历算法
12.5.1广度优先遍历算法基本思想
类似于一个分层搜索的过程,广度优先遍历算法需要使用一个队列以保证访问过的结点的顺序,以便按这个顺序表来访问这些节点的邻接节点
12.5.2广度优先遍历算法步骤
- 访问初始节点v并标记节点v已访问
- 节点v入队列
- 当队列为非空时,继续执行,否则算法结束
- 出队列,取得队列头节点u
- 查找结点u的第一个邻接节点w
- 若节点u的邻接节点w不存在,则转到步骤3;否则循环以下步骤:
- 若节点w上未被访问,则节点w标记为已访问
- 节点w入队列
- 查找节点u的继w邻接节点之后的下一个邻接节点w,转到步骤6
12.6图的遍历
代码:
public class Graph {
private ArrayList<String> vertexList; //存储顶点集合
private int[][] edges; //存储图对应的邻结矩阵
private int numOfEdges; //表示边的数目
//定义给数组boolean[], 记录某个结点是否被访问
private boolean[] isVisited;
public static void main(String[] args) {
//测试一把图是否创建ok
int n = 8; //结点的个数
String Vertexs[] = {"1", "2", "3", "4", "5", "6", "7", "8"};
//创建图对象
Graph graph = new Graph(n);
//循环的添加顶点
for(String vertex: Vertexs) {
graph.insertVertex(vertex);
}
//更新边的关系
graph.insertEdge(0, 1, 1);
graph.insertEdge(0, 2, 1);
graph.insertEdge(1, 3, 1);
graph.insertEdge(1, 4, 1);
graph.insertEdge(3, 7, 1);
graph.insertEdge(4, 7, 1);
graph.insertEdge(2, 5, 1);
graph.insertEdge(2, 6, 1);
graph.insertEdge(5, 6, 1);
//显示一把邻结矩阵
graph.showGraph();
//测试一把,我们的dfs遍历是否ok
System.out.println("深度遍历");
graph.dfs();
System.out.println();
System.out.println("广度优先!");
graph.bfs();
}
//构造器
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
}
//得到第一个邻接结点的下标 w
/**
*
* @param index
* @return 如果存在就返回对应的下标,否则返回-1
*/
public int getFirstNeighbor(int index) {
for(int j = 0; j < vertexList.size(); j++) {
if(edges[index][j] > 0) {
return j;
}
}
return -1;
}
//根据前一个邻接结点的下标来获取下一个邻接结点
public int getNextNeighbor(int v1, int v2) {
for(int j = v2 + 1; j < vertexList.size(); j++) {
if(edges[v1][j] > 0) {
return j;
}
}
return -1;
}
//深度优先遍历算法
//i 第一次就是 0
private void dfs(boolean[] isVisited, int i) {
//首先我们访问该结点,输出
System.out.print(getValueByIndex(i) + "->");
//将结点设置为已经访问
isVisited[i] = true;
//查找结点i的第一个邻接结点w
int w = getFirstNeighbor(i);
while(w != -1) {//说明有
if(!isVisited[w]) {
dfs(isVisited, w);
}
//如果w结点已经被访问过
w = getNextNeighbor(i, w);
}
}
//对dfs 进行一个重载, 遍历我们所有的结点,并进行 dfs
public void dfs() {
isVisited = new boolean[vertexList.size()];
//遍历所有的结点,进行dfs[回溯]
for(int i = 0; i < getNumOfVertex(); i++) {
if(!isVisited[i]) {
dfs(isVisited, i);
}
}
}
//对一个结点进行广度优先遍历的方法
private void bfs(boolean[] isVisited, int i) {
int u ; // 表示队列的头结点对应下标
int w ; // 邻接结点w
//队列,记录结点访问的顺序
LinkedList queue = new LinkedList();
//访问结点,输出结点信息
System.out.print(getValueByIndex(i) + "=>");
//标记为已访问
isVisited[i] = true;
//将结点加入队列
queue.addLast(i);
while( !queue.isEmpty()) {
//取出队列的头结点下标
u = (Integer)queue.removeFirst();
//得到第一个邻接结点的下标 w
w = getFirstNeighbor(u);
while(w != -1) {//找到
//是否访问过
if(!isVisited[w]) {
System.out.print(getValueByIndex(w) + "=>");
//标记已经访问
isVisited[w] = true;
//入队
queue.addLast(w);
}
//以u为前驱点,找w后面的下一个邻结点
w = getNextNeighbor(u, w); //体现出我们的广度优先
}
}
}
//遍历所有的结点,都进行广度优先搜索
public void bfs() {
isVisited = new boolean[vertexList.size()];
for(int i = 0; i < getNumOfVertex(); i++) {
if(!isVisited[i]) {
bfs(isVisited, i);
}
}
}
//图中常用的方法
//返回结点的个数
public int getNumOfVertex() {
return vertexList.size();
}
//显示图对应的矩阵
public void showGraph() {
for(int[] link : edges) {
System.err.println(Arrays.toString(link));
}
}
//得到边的数目
public int getNumOfEdges() {
return numOfEdges;
}
//返回结点i(下标)对应的数据 0->"A" 1->"B" 2->"C"
public String getValueByIndex(int i) {
return vertexList.get(i);
}
//返回v1和v2的权值
public int getWeight(int v1, int v2) {
return edges[v1][v2];
}
//插入结点
public void insertVertex(String vertex) {
vertexList.add(vertex);
}
//添加边
/*
*
* @param v1 表示点的下标即使第几个顶点 "A"-"B" "A"->0 "B"->1
* @param v2 第二个顶点对应的下标
* @param weight 表示
*/
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
}
|