数据结构和算法笔记
数据结构
数据结构包括:线性结构和非线性结构。
线性结构
数据元素之间存在一对一的线性关系,有两种不同的存储结构:顺序存储结构和链式存储结构 。
- 顺序存储结构:顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的。
- 链式存储结构:链式存储结构的线性表称为链表,链表存储的元素不一定连续。
- 线性结构常见有:数组、队列、链表和栈。
非线性结构
包括二维数组、多维数组、广义表、树结构、图结构。
稀疏数组
二维数组转稀疏数组的思路:
- 遍历原始的二维数组,得到有效数据的个数sum;
- 根据sum就可以创建稀疏数组
sparseArr int[sum + 1][3]
- 将二维数组的有效数据存入到稀疏数组。
稀疏数组转原始的二维数组的思路:
- 先读取稀疏数组的第一行,根据第一行的数据,创建原始的二维数组。
- 再读取稀疏数组后几行数据,并赋值原始的二维数组即可。
队列
队列是一个有序列表,可以用数组或是链表来实现。
遵循先入先出的原则,即先存入队列的数组,要先取出,后存入队列的数据后取出。
数组模拟队列思路:
front变量和rear变量分别记录队列前后端下标,存数据是从rear变量存,取数据从front变量取。
rear是队列最后(含),初始值-1;
front是队列最前面元素(不含),指向队列头的前一个位置,初始值-1。
- 当我们将数据存入队列时称为“addQueue”,addQueue的处理有两个步骤:
- 将尾指针往后移:rear+1,当front==rear 表示空;
- 若尾指针rear小于队列的最大下标maxSize-1,则将数据存入rear所指的数组元素中,否则无法存入数据。rear==maxSize-1 表示队列满。
- 代码实现:
import java.util.Scanner;
public class ArrayQueueDemo{
public static void main(String[] args){
//测试
ArrayQueue queue = new ArrayQueue(3);
char key = ' '; //接收用户输入
Scanner scanner = new Scanner(System.in); //接收
boolean loop = true;
//输出一个菜单
while(loop){
System.out.println("s(show):显示队列");
System.out.println("e(exit):退出");
System.out.println("a(add):添加数据");
System.out.println("g(get):取数据");
System.out.println("h(head):查看队列头数据");
key = scanner.next().charAt(0); //接收一个字符
switch(key){
case 's':
queue.showQueue();
break;
case 'a':
System.out.println("输入一个数");
int value = scanner.nextInt();
queue.addQueue(value);
break;
case 'g': //取数据
try{
int res = queue.getQueue();
System.out.printf("取出的数据是%d",res);
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case 'h': //查看队列头数据
try{
int res = queue.headQueue();
System.out.printf("队列头的数据是%d",res);
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case 'e':
scanner.close();
loop=false;
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
//使用数组模拟队列
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; //指向队列头部数据前一个位置
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",i,arr[i]);
}
}
//显示队列的头,注意不是取出数据
public int headQueue(){
//判断队列是否为空
if (isEmpty()){
throw new RuntimeException("队列空,没有数据");
}
return arr[front+1];
}
}
问题分析并优化:
- 数组使用一次就不能复用;
- 将这个数组改成环形数组 %
数组模拟环形队列:
链表
是一个有序列表。
单链表:
小结:
- 链表是以节点的方式存储的;
- 每个节点包含data域和next域,next域指向下一个节点;
- 如图:链表的各个节点不一定是连续存放的。
- 链表分为带头节点的链表和不带头节点的链表,根据实际需求来确定。
单链表的逻辑结构图:
单链表创建和添加:
public class SingleLinkedListDemo{
public static void main(String[] args){
//进行测试
//先创建节点
HeroNode hero1=new HeroNode(1,"宋江","及时雨");
HeroNode hero2=new HeroNode(2,"卢俊义","玉麒麟");
HeroNode hero3=new HeroNode(3,"吴用","智多星");
HeroNode hero4=new HeroNode(4,"林冲","豹子头");
//创建列表,加入
SingleLinkedList singleLinkedList=new SingleLinkedList();
singleLinkedList.add(hero1);
singleLinkedList.add(hero2);
singleLinkedList.add(hero3);
singleLinkedList.add(hero4);
//显示
singleLinkedList.list();
}
}
//定义SingleLinkedList管理我们的英雄
class SingleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体数据
private HeroNode head = new HeroNode(0, "", "");
//添加节点到单向列表
//当不考虑编号顺序时
//1.找到当前链表的最后节点
//2.将最后这个节点的next指向新的节点
public void add(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助变量
HeroNode temp = head;
//遍历链表,找到最后
while (true) {
//找到链表最后
if (temp.next == null) {
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//当退出while循环时,temp指向了链表最后
temp.next = heroNode;
}
//通过遍历显示链表
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此需要辅助变量来遍历
HeroNode temp = head.next;
while (true) {
//判断是否到链表最后
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将next后移,否则死循环
temp = temp.next;
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; //指向下一个节点
//构造器
public HeroNode(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 + "]";
}
}
按顺序添加节点:
public class SingleLinkedListDemo{
public static void main(String[] args){
//进行测试
//先创建节点
HeroNode hero1=new HeroNode(1,"宋江","及时雨");
HeroNode hero2=new HeroNode(2,"卢俊义","玉麒麟");
HeroNode hero3=new HeroNode(3,"吴用","智多星");
HeroNode hero4=new HeroNode(4,"林冲","豹子头");
//创建列表,加入
SingleLinkedList singleLinkedList=new SingleLinkedList();
// singleLinkedList.add(hero1);
// singleLinkedList.add(hero2);
// singleLinkedList.add(hero3);
// singleLinkedList.add(hero4);
//按顺序排列
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero4);
//显示
singleLinkedList.list();
}
}
//定义SingleLinkedList管理我们的英雄
class SingleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体数据
private HeroNode head = new HeroNode(0, "", "");
//添加节点到单向列表
//当不考虑编号顺序时
//1.找到当前链表的最后节点
//2.将最后这个节点的next指向新的节点
public void add(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助变量
HeroNode temp = head;
//遍历链表,找到最后
while (true) {
//找到链表最后
if (temp.next == null) {
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//当退出while循环时,temp指向了链表最后
temp.next = heroNode;
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode heroNode){
//因为头节点不能动,因此我们仍然通过一个辅助变量来帮助找到添加位置
//因为单链表,因此我们找的temp是位于添加位置的前一个节点,否则插入失败
HeroNode temp = head;
boolean flag = false; //flag标志添加的编号是否存在,默认位false
while(true){
if(temp.next == null){ //说明temp已经在链表的最后
break;
}
if(temp.next.no > heroNode.no){ //位置找到了,就在temp后边
break;
}
else if (temp.next.no == heroNode.no){//说明希望添加的heroNode的编号已然存在
flag = true; //说明编号存在
break;
}
temp=temp.next; //后移,遍历当前的链表
}
//判断flag的值
if (flag == true){ //不能添加,说明编号存在
System.out.printf("准备添加的英雄编号%d已经存在了,不能加入\n",heroNode.no);
}
else{
//插入到链表中,temp的后边
heroNode.next = temp.next;
temp.next=heroNode;
}
}
//通过遍历显示链表
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此需要辅助变量来遍历
HeroNode temp = head.next;
while (true) {
//判断是否到链表最后
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将next后移,否则死循环
temp = temp.next;
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; //指向下一个节点
//构造器
public HeroNode(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 + "]";
}
}
头插法(逆序)
尾插法(顺序)
修改节点:
public class SingleLinkedListDemo{
public static void main(String[] args){
//进行测试
//先创建节点
HeroNode hero1=new HeroNode(1,"宋江","及时雨");
HeroNode hero2=new HeroNode(2,"卢俊义","玉麒麟");
HeroNode hero3=new HeroNode(3,"吴用","智多星");
HeroNode hero4=new HeroNode(4,"林冲","豹子头");
//创建列表,加入
SingleLinkedList singleLinkedList=new SingleLinkedList();
//加入
// singleLinkedList.add(hero1);
// singleLinkedList.add(hero2);
// singleLinkedList.add(hero3);
// singleLinkedList.add(hero4);
//按顺序加入
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero4);
//显示
singleLinkedList.list();
//测试修改节点代码
HeroNode newHeroNode = new HeroNode(2,"小卢","玉麒麟~");
singleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况");
singleLinkedList.list();
}
}
//定义SingleLinkedList管理我们的英雄
class SingleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体数据
private HeroNode head = new HeroNode(0, "", "");
//添加节点到单向列表
//当不考虑编号顺序时
//1.找到当前链表的最后节点
//2.将最后这个节点的next指向新的节点
public void add(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助变量
HeroNode temp = head;
//遍历链表,找到最后
while (true) {
//找到链表最后
if (temp.next == null) {
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//当退出while循环时,temp指向了链表最后
temp.next = heroNode;
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode heroNode){
//因为头节点不能动,因此我们仍然通过一个辅助变量来帮助找到添加位置
//因为单链表,因此我们找的temp是位于添加位置的前一个节点,否则插入失败
HeroNode temp = head;
boolean flag = false; //flag标志添加的编号是否存在,默认位false
while(true){
if(temp.next == null){ //说明temp已经在链表的最后
break;
}
if(temp.next.no > heroNode.no){ //位置找到了,就在temp后边
break;
}
else if (temp.next.no == heroNode.no){//说明希望添加的heroNode的编号已然存在
flag = true; //说明编号存在
break;
}
temp=temp.next; //后移,遍历当前的链表
}
//判断flag的值
if (flag == true){ //不能添加,说明编号存在
System.out.printf("准备添加的英雄编号%d已经存在了,不能加入\n",heroNode.no);
}
else{
//插入到链表中,temp的后边
heroNode.next = temp.next;
temp.next=heroNode;
}
}
//修改节点信息,根据no编号来修改,即no编号不能修改
//说明
//1.根据newHeroNode的no来修改即可
public void update(HeroNode newHeroNode){
//判断是否空
if(head.next == null){
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
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.printf("没有找到编号 %d 的节点,不能修改\n",newHeroNode.no);
}
}
//通过遍历显示链表
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此需要辅助变量来遍历
HeroNode temp = head.next;
while (true) {
//判断是否到链表最后
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将next后移,否则死循环
temp = temp.next;
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; //指向下一个节点
//构造器
public HeroNode(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 + "]";
}
}
删除节点:
public class SingleLinkedListDemo{
public static void main(String[] args){
//进行测试
//先创建节点
HeroNode hero1=new HeroNode(1,"宋江","及时雨");
HeroNode hero2=new HeroNode(2,"卢俊义","玉麒麟");
HeroNode hero3=new HeroNode(3,"吴用","智多星");
HeroNode hero4=new HeroNode(4,"林冲","豹子头");
//创建链表
SingleLinkedList singleLinkedList=new SingleLinkedList();
//加入
// singleLinkedList.add(hero1);
// singleLinkedList.add(hero2);
// singleLinkedList.add(hero3);
// singleLinkedList.add(hero4);
//按顺序加入
singleLinkedList.addByOrder(hero1);
singleLinkedList.addByOrder(hero3);
singleLinkedList.addByOrder(hero4);
singleLinkedList.addByOrder(hero2);
singleLinkedList.addByOrder(hero4);
//显示
singleLinkedList.list();
//测试修改节点代码
HeroNode newHeroNode = new HeroNode(2,"小卢","玉麒麟~");
singleLinkedList.update(newHeroNode);
System.out.println("修改后的链表情况");
singleLinkedList.list();
//删除一个节点
singleLinkedList.del(1);
singleLinkedList.del(4);
System.out.println("删除后的链表情况");
singleLinkedList.list();
}
}
//定义SingleLinkedList管理我们的英雄
class SingleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体数据
private HeroNode head = new HeroNode(0, "", "");
//添加节点到单向列表
//当不考虑编号顺序时
//1.找到当前链表的最后节点
//2.将最后这个节点的next指向新的节点
public void add(HeroNode heroNode) {
//因为head节点不能动,因此需要一个辅助变量
HeroNode temp = head;
//遍历链表,找到最后
while (true) {
//找到链表最后
if (temp.next == null) {
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//当退出while循环时,temp指向了链表最后
temp.next = heroNode;
}
//第二种方式在添加英雄时,根据排名将英雄插入到指定位置
//(如果有这个排名,则添加失败,并给出提示)
public void addByOrder(HeroNode heroNode){
//因为头节点不能动,因此我们仍然通过一个辅助变量来帮助找到添加位置
//因为单链表,因此我们找的temp是位于添加位置的前一个节点,否则插入失败
HeroNode temp = head;
boolean flag = false; //flag标志添加的编号是否存在,默认位false
while(true){
if(temp.next == null){ //说明temp已经在链表的最后
break;
}
if(temp.next.no > heroNode.no){ //位置找到了,就在temp后边
break;
}
else if (temp.next.no == heroNode.no){//说明希望添加的heroNode的编号已然存在
flag = true; //说明编号存在
break;
}
temp=temp.next; //后移,遍历当前的链表
}
//判断flag的值
if (flag == true){ //不能添加,说明编号存在
System.out.printf("准备添加的英雄编号%d已经存在了,不能加入\n",heroNode.no);
}
else{
//插入到链表中,temp的后边
heroNode.next = temp.next;
temp.next=heroNode;
}
}
//修改节点信息,根据no编号来修改,即no编号不能修改
//说明
//1.根据newHeroNode的no来修改即可
public void update(HeroNode newHeroNode){
//判断是否空
if(head.next == null){
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
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.printf("没有找到编号 %d 的节点,不能修改\n",newHeroNode.no);
}
}
//删除节点
//思路
//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;
}
//判断flag
if(flag){
//可以删除
temp.next = temp.next.next;
}
else{
System.out.printf("要删除的 %d 节点不存在\n",no);
}
}
//通过遍历显示链表
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此需要辅助变量来遍历
HeroNode temp = head.next;
while (true) {
//判断是否到链表最后
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将next后移,否则死循环
temp = temp.next;
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
class HeroNode {
public int no;
public String name;
public String nickname;
public HeroNode next; //指向下一个节点
//构造器
public HeroNode(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 + "]";
}
}
双向链表:
单向链表问题:
- 单向链表查找的方向只能是一个方向,而双向链表可以向前或者向后查找。
- 单向链表不能自我删除,需要靠辅助节点,而双向链表可以自我删除,所以单链表的删除需要通过temp(待删除节点的前一个结点)来删除。
public class DoubleLinkedListDemo {
public static void main(String[] args){
//测试
System.out.println("双向链表的测试");
//先创建节点
HeroNode2 hero1=new HeroNode2(1,"宋江","及时雨");
HeroNode2 hero2=new HeroNode2(2,"卢俊义","玉麒麟");
HeroNode2 hero3=new HeroNode2(3,"吴用","智多星");
HeroNode2 hero4=new HeroNode2(4,"林冲","豹子头");
//创建链表
DoubleLinkedList doubleLinkedList = new DoubleLinkedList();
//添加节点
doubleLinkedList.add(hero1);
doubleLinkedList.add(hero2);
doubleLinkedList.add(hero3);
doubleLinkedList.add(hero4);
doubleLinkedList.list();
//修改
HeroNode2 newHeroNode=new HeroNode2(4,"公孙胜","入云龙");
doubleLinkedList.update(newHeroNode);
System.out.println("修改后链表情况");
doubleLinkedList.list();
//删除
doubleLinkedList.del(3);
System.out.println("删除后链表情况");
doubleLinkedList.list();
}
}
//创建一个双向链表
class DoubleLinkedList {
//先初始化一个头节点,头节点不要动,不存放具体数据
private HeroNode2 head = new HeroNode2(0, "", "");
//返回头节点
public HeroNode2 gethead(){
return head;
}
//遍历双向链表的方法
//通过遍历显示链表
public void list() {
//判断链表是否为空
if (head.next == null) {
System.out.println("链表为空");
return;
}
//因为头节点不能动,因此需要辅助变量来遍历
HeroNode2 temp = head.next;
while (true) {
//判断是否到链表最后
if (temp == null) {
break;
}
//输出节点信息
System.out.println(temp);
//将next后移,否则死循环
temp = temp.next;
}
}
//添加一个节点到双向链表的最后
public void add(HeroNode2 heroNode) {
//因为head节点不能动,因此需要一个辅助变量
HeroNode2 temp = head;
//遍历链表,找到最后
while (true) {
//找到链表最后
if (temp.next == null) {
break;
}
//如果没有找到最后,将temp后移
temp = temp.next;
}
//当退出while循环时,temp指向了链表最后
//构成一个双向链表
temp.next = heroNode;
heroNode.pre = temp;
}
//修改一个节点的内容,和单向链表一样
public void update(HeroNode2 newHeroNode){
//判断是否空
if(head.next == null){
System.out.println("链表为空");
return;
}
//找到需要修改的节点,根据no编号
//定义一个辅助变量
HeroNode2 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.printf("没有找到编号 %d 的节点,不能修改\n",newHeroNode.no);
}
}
//从双向链表中删除一个节点
//说明
//1.对于双向链表,我们可以直接找到要删除的节点,而非前一个节点
//2.找到后自我删除即可
public void del(int no){
//判断当前链表是否非空
if (head.next == null){
System.out.println("链表为空,无法删除");
}
HeroNode2 temp = head.next; //赋值变量
boolean flag = false; //标志是否找到待删除的节点
while(true){
if(temp == null){ //已经到链表的最后节点
break;
}
if(temp.no == no){
//找到要删除的节点
flag = true;
break;
}
temp = temp.next; //temp后移,遍历
}
//判断flag
if(flag){
//可以删除
temp.pre.next = temp.next;
//这里代码有问题
//如果是最后一个节点则不需要执行下面这句话,否则出现空指针异常
//temp.next.pre ===> null.pre
if (temp.next != null){
temp.next.pre = temp.pre;
}
}
else{
System.out.printf("要删除的 %d 节点不存在\n",no);
}
}
}
//定义HeroNode,每个HeroNode对象就是一个节点
class HeroNode2 {
public int no;
public String name;
public String nickname;
public HeroNode2 next; //指向下一个节点
public HeroNode2 pre; //指向前一个节点
//构造器
public HeroNode2(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 + "]";
}
}
单向环形链表
约瑟夫问题
单向环形链表创建和遍历:
小孩出圈:
public class Josepfu {
public static void main(String[] args){
//测试一把构建和遍历环形链表
CircleSingleLinkedList circleSingleLinkedList = new CircleSingleLinkedList();
circleSingleLinkedList.addBoy(5); //加入5个小孩节点
circleSingleLinkedList.showBoy();
//测试一把小孩出圈是否正确
circleSingleLinkedList.countBoy(1,2,5);
}
}
//创建一个环形的单向链表
class CircleSingleLinkedList{
//创建一个first节点,当前还没有编号
private Boy first = new Boy(-1);
//添加小孩节点,构建成环形链表
public void addBoy(int nums){
//nums做一个数据校验
if (nums < 1){
System.out.println("nums的值不正确");
return;
}
Boy curBoy = null; //辅助变量,帮助构建环形链表
//使用for循环创建环形链表
for(int i = 1; i<=nums; i++){
//根据编号创建小孩节点
Boy boy = new Boy(i);
//如果是第一个小孩
if(i == 1){
first = boy;
first.setNext(first); //构成一个环
curBoy=first; //让curBoy指向第一个小孩
}
else{
curBoy.setNext(boy);
boy.setNext(first);
curBoy = boy;
}
}
}
//遍历当前所有节点
public void showBoy(){
//判断链表是否为空
if(first == null){
System.out.println("链表为空");
return;
}
//因为first不能动,因此仍然使用辅助指针curBoy完成遍历
Boy curBoy = first;
while(true){
System.out.printf("小孩的编号 %d \n",curBoy.getNo());
if(curBoy.getNext()==first){ //说明已经遍历完毕
break;
}
curBoy=curBoy.getNext(); //curBoy后移
}
}
//根据用户输入,计算出小孩出圈的顺序
public void countBoy(int startNo, int countNum, int nums){
//先对数据进行校验
if(first == null || startNo<1 || startNo>nums){
System.out.println("参数输入有误,请重新输入");
return;
}
//创建一个辅助指针,帮助完成小孩出圈
Boy helper = first;
//需要创建一个辅助变量helper,事先应该指向环形链表的最后这个节点
while(true){
if(helper.getNext()==first){ //helper指向了最后节点
break;
}
helper = helper.getNext();
}
//小孩报数前先让first和helper移动k-1次
for(int j=0;j<startNo-1;j++){
first = first.getNext();
helper = helper.getNext();
}
//当小孩报数时,让first和helper指针同时移动m-1次,然后出圈
//这里是一个循环操作,直到圈中只有一个节点
while(true){
if(helper == first){ //说明圈中只有一个节点
break;
}
//让first和helper同时移动countNum-1次
for(int j =0;j<countNum-1;j++){
first = first.getNext();
helper = helper.getNext();
}
//这是first指向的节点,就是要出圈的节点
System.out.printf("小孩 %d 出圈\n",first.getNo());
//这是将first指向的小孩节点出圈
first = first.getNext();
helper.setNext(first);
}
System.out.printf("最后留在圈中的小孩编号 %d",first.getNo());
}
}
//创建一个boy类,表示一个节点
class Boy{
private int no; //编号
private Boy next; //指向下一个节点,默认为空
//构造方法
public Boy(int no){
this.no = no;
}
public int getNo(){
return no;
}
public void setNo(int no){
this.no = no;
}
public Boy getNext(){
return next;
}
public void setNext(Boy next){
this.next = next;
}
}
栈
- 是一个先入后出的有序列表;
- 栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。
- 根据栈的定义,最先放入栈中的元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素先删除,最先放入的元素最后删除。
出栈(pop)和入栈(push)示意图:
数组实现栈:
import java.util.Scanner;
public class ArrayStackDemo {
public static void main(String[] args){
//测试一下ArrayStack是否正确
//先创建一个ArrayStack对象->表示栈
ArrayStack stack = new ArrayStack(4);
String key="";
boolean loop = true; //控制是否退出菜单
Scanner scanner = new Scanner(System.in);
while(loop){
System.out.println("show:表示显示栈");
System.out.println("exit:退出程序");
System.out.println("push:入栈");
System.out.println("pop:出栈");
System.out.println("请输入你的选择");
key = scanner.next();
switch(key){
case "show":
stack.list();
break;
case "push":
System.out.println("请输入一个数");
int value = scanner.nextInt();
stack.push(value);
break;
case "pop":
try{
int res = stack.pop();
System.out.printf("出栈的数据是 %d\n",res);
}catch(Exception e){
System.out.println(e.getMessage());
}
break;
case "exit":
scanner.close();
loop = false;
break;
default:
break;
}
}
System.out.println("程序退出");
}
}
//定义一个ArrayStack表示栈
class ArrayStack{
private int maxSize; //栈的大小
private int[] stack; //数组模拟栈
private int top = -1; //top表示栈顶,初始化为-1
//构造器
public ArrayStack(int maxSize){
this.maxSize = maxSize;
stack = new int[this.maxSize];
}
//栈满
public boolean isFull(){
return top==maxSize-1;
}
//栈空
public boolean isEmpty(){
return top==-1;
}
//入栈-push
public void push(int value){
//先判断栈是否满
if(isFull()){
System.out.println("栈满");
return;
}
top++;
stack[top]=value;
}
//出栈-pop,将栈的数据返回
public int 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]);
}
}
}
应用:综合计算器
哈希表
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
import java.util.Scanner;
public class HashTabDemo {
public static void main(String[] args){
//创建哈希表
HashTab hashTab=new HashTab(7);
//写一个简单的菜单
String key="";
Scanner scanner=new Scanner(System.in);
while(true){
System.out.println("add:添加雇员");
System.out.println("find:查找雇员");
System.out.println("list:显示雇员");
System.out.println("exit:退出");
key = scanner.next();
switch (key){
case "add":
System.out.println("输入id");
int id=scanner.nextInt();
System.out.println("输入名字");
String name=scanner.next();
//创建雇员
Emp emp=new Emp(id,name);
hashTab.add(emp);
break;
case "find":
System.out.println("请输入查找雇员id");
id=scanner.nextInt();
hashTab.findEmpById(id);
break;
case "list":
hashTab.list();
break;
case "exit":
scanner.close();
System.exit(0);
break;
default:
break;
}
}
}
}
//创建HashTab 管理多条链表
class HashTab {
private EmpLinkedList[] empLinkedListArray;
private int size; //表示多少条链表
//构造器
public HashTab(int size){
this.size=size;
//初始化empLinkedListArray
empLinkedListArray=new EmpLinkedList[size];
//留一个坑,这时不要忘了分别初始化每个链表
for (int i=0;i<size;i++){
empLinkedListArray[i]=new EmpLinkedList();
}
}
//添加雇员
public void add(Emp emp){
//根据员工的id,得到该员工应当添加到哪条链表
int empLinkListNo=hashFun(emp.id);
//将emp添加到对应的链表中
empLinkedListArray[empLinkListNo].add(emp);
}
//遍历所有的链表,遍历HashTable
public void list(){
for (int i=0;i<size;i++){
empLinkedListArray[i].list(i);
}
}
//编写散列函数,使用简单的取模法
public int hashFun(int id){
return id % size;
}
//根据输入的id,查找雇员
public void findEmpById(int id){
//使用散列函数确定到哪条链表查找
int empLinkListNo=hashFun(id);
Emp emp=empLinkedListArray[empLinkListNo].findEmpById(id);
if (emp!=null){ //找到了
System.out.printf("在第 %d 条链表中,找到雇员id = %d\n",empLinkListNo,id);
}
else {
System.out.println("在哈希表中,没有找到该雇员\n");
}
}
}
//表示一个雇员
class Emp{
public int id;
public String name;
public Emp next;
public Emp(int id, String name){
this.id=id;
this.name=name;
}
}
//创建EmpLinkedList,表示链表
class EmpLinkedList{
//头指针,执行第一个Emp,因此我们这个链表的head是直接指向第一个Emp
private Emp head; //默认为null
//添加雇员到链表
//说明
//1.假定添加雇员的时候,id的分配总是从小到大,因此直接将雇员添加到链表的最后
public void add(Emp emp){
//如果是添加第一个雇员
if (head == null){
head = emp;
return;
}
//如果不是第一个雇员,则使用一个辅助的指针,帮助定位到最后
Emp curEmp = head;
while (true){
if(curEmp.next==null){
break;
}
curEmp=curEmp.next; //后移
}
//退出时,直接将emp加入链表即可
curEmp.next=emp;
}
//遍历链表的雇员信息
public void list(int no){
if(head == null){ //说明链表为空
System.out.println("第 "+no+" 条链表为空");
return;
}
System.out.print("第 "+no+" 条链表的信息为");
Emp curEmp=head; //辅助指针
while (true){
System.out.printf("=> id=%d name=%s\t",curEmp.id,curEmp.name);
if (curEmp.next==null){ //说明curEmp已经是最后的节点
break;
}
curEmp=curEmp.next;
}
System.out.println();
}
//根据id查找雇员
//如果查找到,就返回Emp,否则返回为空
public Emp findEmpById(int id){
//判断链表是否为空
if (head == null){
System.out.println("链表为空");
return null;
}
//辅助指针
Emp curEmp=head;
while (true){
if(curEmp.id==id){ //找到
break; //这是curEmp就指向要查找的雇员
}
//退出
if (curEmp.next==null){ //说明遍历当前链表没有找到该雇员
curEmp = null;
break;
}
}
return curEmp;
}
}
树
为什么需要树这种数据结构:
- 数组存储方式的分析:
- 优点:通过下标方式访问元素,速度快。对于有序数组,还可以使用二分查找提高检索效率。
- 缺点:如果要插入某个值或者删除某个值,会整体移动,效率低。
- 链式存储结构
- 优点:在一定程度上对数组存储方式有优化(比如,插入一个节点时,只需要将插入节点链接到链表中即可,删除效率也很高)。
- 缺点:在进行检索时,效率很低(比如,检索某个值,需要从头节点开始遍历)。
- 树存储结构
- 能提高数据的存储、读取的效率,比如利用二叉排序树,既可以保证数据的检索速度,同时也可以保证数据的插入、删除和修改的速度。
树的基本术语:
- 节点
- 根节点(root节点)
- 父节点
- 子节点
- 叶子节点(没有子节点的节点)
- 节点的权(节点值/路径值)
- 路径(从root节点找到该节点的路线)
- 层
- 子树
- 树的高度(最大层数)
- 森林:多颗子树构成森林
二叉树
二叉树的概念
- 树有很多种,每个节点最多只能有两个子节点(可以只含有一个子节点)的一种形式称为二叉树。
- 二叉树的子节点分为左节点和右节点。
- 如果该二叉树的所有叶子节点都在最后一层,并且节点总数=2^n-1,n为层数,则我们称为满二叉树。其中第 i 层的节点数为2^(i-1)。
- 如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。(百科定义:树中所含的n个节点和满二叉树中编号为1至n的节点一一对应,即树中节点连续。)
二叉树遍历
- 前序遍历:先输出父节点,再遍历左子树和右子树。
- 思路:
- 先输出当前节点(初始的时候是root节点);
- 如果左子节点不为空,则递归继续前序遍历;
- 如果右子节点不为空,则递归继续前序遍历。
- 思路:
- 中序遍历:先遍历左子树,再遍历父节点,再遍历右子树。
- 思路:
- 如果左子节点不为空,则递归继续中序遍历;
- 输出当前节点;
- 如果右子节点不为空,则递归继续中序遍历。
- 思路:
- 后序遍历:先遍历左子树,再遍历右子树,最后输出父节点。
- 思路:
- 如果左子节点不为空,则递归继续后序遍历;
- 如果右子节点不为空,则递归继续后序遍历;
- 输出当前节点。
- 思路:
- 小结:看输出父节点的顺序,就确定是前序、中序还是后续
package tree;
public class BinaryTreeDemo {
public static void main(String[] args){
//先创建一颗二叉树
BinaryTree binaryTree=new BinaryTree();
//创建节点
HeroNode root=new HeroNode(1,"宋江");
HeroNode node2=new HeroNode(2,"吴用");
HeroNode node3=new HeroNode(3,"卢俊义");
HeroNode node4=new HeroNode(4,"林冲");
HeroNode node5=new HeroNode(5,"关胜");
//先手动创建二叉树,后面学习递归方式创建二叉树
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
//测试
System.out.println("前序遍历"); //1,2,3,5,4
binaryTree.preOrder();
System.out.println("中序遍历"); //2,1,5,3,4
binaryTree.infixOrder();
System.out.println("后序遍历"); //2,5,4,3,1
binaryTree.postOrder();
}
}
//定义BinaryTree
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
//前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
}
else {
System.out.println("二叉树为空,无法遍历");
}
}
//中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
}
else {
System.out.println("二叉树为空,无法遍历");
}
}
//后序遍历
public void postOrder() {
if (this.root != null){
this.root.postOrder();
}
else{
System.out.println("二叉树为空,无法遍历");
}
}
}
//先创建HeroNode节点
class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
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 "tree.HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
//编写前序遍历的方法
public void preOrder() {
System.out.println(this); //先输出父节点
//递归向左子树前序遍历
if (this.left != null) {
this.left.preOrder();
}
//递归向右子树前序遍历
if (this.right != null) {
this.right.preOrder();
}
}
//中序遍历
public void infixOrder() {
//递归向左子树中序遍历
if (this.left != null) {
this.left.infixOrder();
}
//输出父节点
System.out.println(this);
//递归向右子树中序遍历
if (this.right != null) {
this.right.infixOrder();
}
}
//后序遍历
public void postOrder() {
if (this.left != null) {
this.left.postOrder();
}
if (this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
}
查找指定节点
- 前序查找
- 先判断当前节点的no是否等于要查找的no,如果相等就返回当前节点。
- 如果不等,则判断当前节点的左子节点是否为空,如果不为空,则递归前序查找。
- 如果左递归前序查找,找到节点,则返回,否则继续判断,当前节点的右子节点是否为空,如果不为空,则继续向右递归前序查找。
- 中序查找
- 判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
- 如果找到,则返回,否则就和当前节点比较,如果找到,则返回当前节点,否则继续进行右递归中序查找
- 如果右递归中序查找,找到就返回,否则返回null
- 后序查找
- 判断当前节点的左子节点是否为空,如果不为空,则左子节点递归后序查找
- 如果找到,就返回,否则,就判断当前节点的右子节点是否为空,如果不为空,则右递归进行后序查找,如果找到,就返回。
- 和当前节点进行比较,如果是则返回,否则返回null
package tree;
public class BinaryTreeDemo {
public static void main(String[] args){
//先创建一颗二叉树
BinaryTree binaryTree=new BinaryTree();
//创建节点
HeroNode root=new HeroNode(1,"宋江");
HeroNode node2=new HeroNode(2,"吴用");
HeroNode node3=new HeroNode(3,"卢俊义");
HeroNode node4=new HeroNode(4,"林冲");
HeroNode node5=new HeroNode(5,"关胜");
//先手动创建二叉树,后面学习递归方式创建二叉树
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
//测试
// System.out.println("前序遍历"); //1,2,3,5,4
// binaryTree.preOrder();
//
// System.out.println("中序遍历"); //2,1,5,3,4
// binaryTree.infixOrder();
//
// System.out.println("后序遍历"); //2,5,4,3,1
// binaryTree.postOrder();
// //前序遍历查找
// //前序遍历查找次数:4
// System.out.println("前序遍历查找");
// HeroNode resNode = binaryTree.preOrderSearch(5);
// if (resNode != null) {
// System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
// }
// else{
// System.out.printf("没有找到 no=%d 的英雄",5);
// }
// //中序遍历查找
// //中序遍历查找次数:3
// System.out.println("中序遍历查找");
// HeroNode resNode = binaryTree.infixOrderSearch(5);
// if (resNode != null) {
// System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
// }
// else{
// System.out.printf("没有找到 no=%d 的英雄",5);
// }
//后序遍历查找
//后序遍历查找次数:2
System.out.println("后序遍历查找");
HeroNode resNode = binaryTree.postOrderSearch(5);
if (resNode != null) {
System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
}
else{
System.out.printf("没有找到 no=%d 的英雄",5);
}
}
}
//定义BinaryTree
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
//前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
}
else {
System.out.println("二叉树为空,无法遍历");
}
}
//中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
}
else {
System.out.println("二叉树为空,无法遍历");
}
}
//后序遍历
public void postOrder() {
if (this.root != null){
this.root.postOrder();
}
else{
System.out.println("二叉树为空,无法遍历");
}
}
//前序遍历查找
public HeroNode preOrderSearch(int no) {
if (this.root != null) {
return this.root.preOrderSearch(no);
}
else {
return null;
}
}
//中序遍历查找
public HeroNode infixOrderSearch(int no) {
if (this.root != null) {
return this.root.infixOrderSearch(no);
}
else{
return null;
}
}
//后序遍历查找
public HeroNode postOrderSearch(int no) {
if (this.root != null) {
return this.root.postOrderSearch(no);
}
else {
return null;
}
}
}
//先创建HeroNode节点
class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
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 "tree.HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
//编写前序遍历的方法
public void preOrder() {
System.out.println(this); //先输出父节点
//递归向左子树前序遍历
if (this.left != null) {
this.left.preOrder();
}
//递归向右子树前序遍历
if (this.right != null) {
this.right.preOrder();
}
}
//中序遍历
public void infixOrder() {
//递归向左子树中序遍历
if (this.left != null) {
this.left.infixOrder();
}
//输出父节点
System.out.println(this);
//递归向右子树中序遍历
if (this.right != null) {
this.right.infixOrder();
}
}
//后序遍历
public void postOrder() {
if (this.left != null) {
this.left.postOrder();
}
if (this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
//前序遍历查找
/**
*
* @param no 待查找的no
* @return 如果找到就返回node,否则返回null
*/
public HeroNode preOrderSearch(int no) {
//比较当前节点是不是no
System.out.println("前序查找次数");
if (this.no == no) {
return this;
}
//1.判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
//2.如果左递归前序查找,找到节点,则返回
HeroNode resNode = null;
if (this.left != null) {
resNode=this.left.preOrderSearch(no);
}
if (resNode != null) { //说明左子节点找到
return resNode;
}
//1.继续判断,当前节点的右子节点是否为空,如果不为空,则继续向右递归前序查找。
if (this.right != null) {
resNode=this.right.preOrderSearch(no);
}
return resNode;
}
//中序遍历查找
public HeroNode infixOrderSearch(int no) {
//判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode=null;
if (this.left != null) {
resNode=this.left.infixOrderSearch(no);
}
if (resNode != null) {
return resNode;
}
//和当前节点比较,如果找到,则返回当前节点,否则继续进行右递归中序查找
System.out.println("中序查找次数");
if (this.no == no) {
return this;
}
if (this.right != null) {
resNode=this.right.infixOrderSearch(no);
}
return resNode;
}
//后序遍历查找
public HeroNode postOrderSearch(int no) {
//判断当前节点的左子节点是否为空,如果不为空,则左子节点递归后序查找
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;
}
//如果左右子树都没有找到,就比较当前节点是不是no
System.out.println("后序查找次数");
if (this.no == no) {
return this;
}
return resNode;
}
}
删除节点
要求:
- 如果删除的节点是叶子节点,则删除该节点
- 如果删除的节点是非叶子节点,则删除该子树
思路:
- 首先考虑如果树是空树,如果只有一个root节点,则等价将二叉树置空
- 因为二叉树是单向的,所以我们是判断当前节点的子节点是否是需要删除的节点,而不能去判断当前这个节点是不是需要删除的节点
- 如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,就将this.left=null,并且返回(结束递归删除)
- 如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,就将this.right=null,并且返回(结束递归删除)
- 如果第2和第3步没有删除节点,那么我们就需要向左子树进行递归删除
- 如果第4步也没有删除节点,则应向右子树进行递归删除
package tree;
public class BinaryTreeDemo {
public static void main(String[] args){
//先创建一颗二叉树
BinaryTree binaryTree=new BinaryTree();
//创建节点
HeroNode root=new HeroNode(1,"宋江");
HeroNode node2=new HeroNode(2,"吴用");
HeroNode node3=new HeroNode(3,"卢俊义");
HeroNode node4=new HeroNode(4,"林冲");
HeroNode node5=new HeroNode(5,"关胜");
//先手动创建二叉树,后面学习递归方式创建二叉树
root.setLeft(node2);
root.setRight(node3);
node3.setRight(node4);
node3.setLeft(node5);
binaryTree.setRoot(root);
// //测试遍历
// System.out.println("前序遍历"); //1,2,3,5,4
// binaryTree.preOrder();
//
// System.out.println("中序遍历"); //2,1,5,3,4
// binaryTree.infixOrder();
//
// System.out.println("后序遍历"); //2,5,4,3,1
// binaryTree.postOrder();
// //前序遍历查找
// //前序遍历查找次数:4
// System.out.println("前序遍历查找");
// HeroNode resNode = binaryTree.preOrderSearch(5);
// if (resNode != null) {
// System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
// }
// else{
// System.out.printf("没有找到 no=%d 的英雄",5);
// }
// //中序遍历查找
// //中序遍历查找次数:3
// System.out.println("中序遍历查找");
// HeroNode resNode = binaryTree.infixOrderSearch(5);
// if (resNode != null) {
// System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
// }
// else{
// System.out.printf("没有找到 no=%d 的英雄",5);
// }
// //后序遍历查找
// //后序遍历查找次数:2
// System.out.println("后序遍历查找");
// HeroNode resNode = binaryTree.postOrderSearch(5);
// if (resNode != null) {
// System.out.printf("找到了,信息为 no=%d name=%s",resNode.getNo(),resNode.getName());
// }
// else{
// System.out.printf("没有找到 no=%d 的英雄",5);
// }
//测试删除节点
System.out.println("删除前,前序遍历");
binaryTree.preOrder(); //1,2,3,5,4
binaryTree.delNode(5);
System.out.println("删除后,前序遍历");
binaryTree.preOrder(); //1,2,3,4
}
}
//定义BinaryTree
class BinaryTree {
private HeroNode root;
public void setRoot(HeroNode root) {
this.root = root;
}
//前序遍历
public void preOrder() {
if (this.root != null) {
this.root.preOrder();
}
else {
System.out.println("二叉树为空,无法遍历");
}
}
//中序遍历
public void infixOrder() {
if (this.root != null) {
this.root.infixOrder();
}
else {
System.out.println("二叉树为空,无法遍历");
}
}
//后序遍历
public void postOrder() {
if (this.root != null){
this.root.postOrder();
}
else{
System.out.println("二叉树为空,无法遍历");
}
}
//前序遍历查找
public HeroNode preOrderSearch(int no) {
if (this.root != null) {
return this.root.preOrderSearch(no);
}
else {
return null;
}
}
//中序遍历查找
public HeroNode infixOrderSearch(int no) {
if (this.root != null) {
return this.root.infixOrderSearch(no);
}
else{
return null;
}
}
//后序遍历查找
public HeroNode postOrderSearch(int no) {
if (this.root != null) {
return this.root.postOrderSearch(no);
}
else {
return null;
}
}
//删除节点
public void delNode(int no) {
if (root !=null) {
//如果只有一个root节点,这里要立刻判断root是不是要删除的节点
if (root.getNo() == no) {
root=null;
}
else {
//递归删除
root.delNode(no);
}
}
else {
System.out.println("这是空树,无法删除");
}
}
}
//先创建HeroNode节点
class HeroNode {
private int no;
private String name;
private HeroNode left;
private HeroNode right;
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 "tree.HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
//编写前序遍历的方法
public void preOrder() {
System.out.println(this); //先输出父节点
//递归向左子树前序遍历
if (this.left != null) {
this.left.preOrder();
}
//递归向右子树前序遍历
if (this.right != null) {
this.right.preOrder();
}
}
//中序遍历
public void infixOrder() {
//递归向左子树中序遍历
if (this.left != null) {
this.left.infixOrder();
}
//输出父节点
System.out.println(this);
//递归向右子树中序遍历
if (this.right != null) {
this.right.infixOrder();
}
}
//后序遍历
public void postOrder() {
if (this.left != null) {
this.left.postOrder();
}
if (this.right != null) {
this.right.postOrder();
}
System.out.println(this);
}
//前序遍历查找
/**
*
* @param no 待查找的no
* @return 如果找到就返回node,否则返回null
*/
public HeroNode preOrderSearch(int no) {
//比较当前节点是不是no
System.out.println("前序查找次数");
if (this.no == no) {
return this;
}
//1.判断当前节点的左子节点是否为空,如果不为空,则递归前序查找
//2.如果左递归前序查找,找到节点,则返回
HeroNode resNode = null;
if (this.left != null) {
resNode=this.left.preOrderSearch(no);
}
if (resNode != null) { //说明左子节点找到
return resNode;
}
//1.继续判断,当前节点的右子节点是否为空,如果不为空,则继续向右递归前序查找。
if (this.right != null) {
resNode=this.right.preOrderSearch(no);
}
return resNode;
}
//中序遍历查找
public HeroNode infixOrderSearch(int no) {
//判断当前节点的左子节点是否为空,如果不为空,则递归中序查找
HeroNode resNode=null;
if (this.left != null) {
resNode=this.left.infixOrderSearch(no);
}
if (resNode != null) {
return resNode;
}
//和当前节点比较,如果找到,则返回当前节点,否则继续进行右递归中序查找
System.out.println("中序查找次数");
if (this.no == no) {
return this;
}
if (this.right != null) {
resNode=this.right.infixOrderSearch(no);
}
return resNode;
}
//后序遍历查找
public HeroNode postOrderSearch(int no) {
//判断当前节点的左子节点是否为空,如果不为空,则左子节点递归后序查找
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;
}
//如果左右子树都没有找到,就比较当前节点是不是no
System.out.println("后序查找次数");
if (this.no == no) {
return this;
}
return resNode;
}
//递归删除节点
//1.如果删除的节点是叶子节点,则删除该节点
//2.如果删除的节点是非叶子节点,则删除该子树
public void delNode(int no) {
//如果当前节点的左子节点不为空,并且左子节点就是要删除的节点,就将this.left=null,并且返回
if (this.left != null && this.left.no==no){
this.left=null;
return;
}
//如果当前节点的右子节点不为空,并且右子节点就是要删除的节点,就将this.right=null,并且返回
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);
}
}
}
二叉树的顺序存储结构
二叉树的存储结构有两种,分别是顺序存储结构和链式存储结构。
顺序存储结构的特点:
-
顺序存储结构要求是完全二叉树。若不是完全二叉树,则需要转化成完全二叉树。如下图,补零填充。
-
第n个索引元素的左子节点索引为 2n+1。(n从0开始)
-
第n个索引元素的右子节点索引为 2n+2
-
第n个索引元素的父节点索引为 (n-1)/2
例:需求,给你一个数组[1,2,3,4,5,6,7],要求以二叉树前序遍历的方式进行遍历。前序遍历的结果应当为 1,2,4,5,3,6,7。
package tree;
public class ArrBinaryTreeDemo {
public static void main(String[] args){
int[] arr={1,2,3,4,5,6,7};
//创建一个ArrBinaryTree
ArrBinaryTree arrBinaryTree=new ArrBinaryTree(arr);
arrBinaryTree.preOrder(); //1,2,4,5,3,6,7
}
}
//编写一个ArrayBinaryTree,实现顺序存储结构
class ArrBinaryTree {
private int[] arr; //存储数据节点
public ArrBinaryTree(int[] arr) {
this.arr=arr;
}
//重载preOrder方法
public void preOrder() {
this.preOrder(0);
}
//编写一个方法,完成二叉树顺序存储结构的前序遍历
/**
*
* @param index 数组的下标
*/
public void preOrder(int index) {
//如果数组为空,或者arr.length=0
if (arr == null || arr.length == 0) {
System.out.println("数组为空,不能按照二叉树的前序遍历");
return;
}
//输出当前这个元素
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);
}
}
}
线索化二叉树
基本介绍:
- n个节点的二叉链表中含有 n+1 【2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该节点在某种遍历次序(前序遍历、中序遍历、后序遍历)下的前驱和后继节点的指针(这种附加的指针称为“线索”)。
- 这种加上了线索的二叉树链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
- 一个节点的前一个节点,称为前驱节点
- 一个节点的后一个节点,称为后继节点
中序遍历结果:[3,1,4,0,2]
说明:当线索化二叉树后,Node节点的属性left和right,有如下情况:
- left指向的是左子树,也可能是指向的前驱节点。比如,0节点left指向的是左子树,4节点left指向的是前驱节点。
- right指向的是右子树,也可能是指向的后继节点。比如,0节点right指向的是右子树,3节点left指向的是后继节点。
package tree.threadedbinarytree;
public class ThreadedBinaryTreeDemo {
public static void main(String[] args) {
HeroNode root = new HeroNode(0,"Tom");
HeroNode node1 = new HeroNode(1,"Jack");
HeroNode node2 = new HeroNode(2,"Smith");
HeroNode node3 = new HeroNode(3,"Mary");
HeroNode node4 = new HeroNode(4,"King");
//手动创建二叉树
root.setLeft(node1);
root.setRight(node2);
node1.setLeft(node3);
node1.setRight(node4);
//测试线索化
ThreadedBinaryTree threadedBinaryTree=new ThreadedBinaryTree();
threadedBinaryTree.setRoot(root);
threadedBinaryTree.threadedNodes();
//以3号节点为例
HeroNode leftNode=node3.getLeft();
System.out.println("3号节点的前驱节点为 "+leftNode);
HeroNode rightNode=node3.getRight();
System.out.println("3号节点的后继节点为 "+rightNode);
}
}
//定义ThreadedBinaryTree 实现线索化功能的二叉树
class ThreadedBinaryTree {
private HeroNode root;
//为了实现线索化,需要创建一个指向当前节点的前驱节点的指针
//在递归进行线索化时,pre总是保留前一个节点
private HeroNode pre = null;
public void setRoot(HeroNode root) {
this.root = root;
}
//重载线索化方法
public void threadedNodes() {
this.threadedNodes(root);
}
//编写对二叉树进行中序线索化的方法
public void threadedNodes(HeroNode node) {
//如果node==null,不能线索化
if (node==null) {
return;
}
//(1)先线索化左子树
threadedNodes(node.getLeft());
//(2)线索化当前节点,有点难度
//处理当前节点的前驱节点
if (node.getLeft() == null) {
//让当前节点的左指针指向前驱节点
node.setLeft(pre);
//修改当前节点的左指针类型,指向前驱节点
node.setLeftType(1);
}
//处理后继节点
if (pre != null && pre.getRight()==null) {
//让前驱节点的右指针指向当前节点
pre.setRight(node);
//修改前驱节点的右指针类型
pre.setRightType(1);
}
//每处理一个节点后,让当前节点是下一个节点的前驱节点
pre = node;
//(3)线索化右子树
threadedNodes(node.getRight());
}
}
//先创建HeroNode节点
class HeroNode {
private int no;
private String name;
private HeroNode left; //默认null
private HeroNode right; //默认null
//说明
//1.如果leftType==0 表示指向的是左子树,如果1 则表示指向前驱节点
//2.如果rightType==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;
}
public int getLeftType() {
return leftType;
}
public void setLeftType(int leftType) {
this.leftType = leftType;
}
public int getRightType() {
return rightType;
}
public void setRightType(int rightType) {
this.rightType = rightType;
}
@Override
public String toString() {
return "tree.HeroNode{" +
"no=" + no +
", name='" + name + '\'' +
'}';
}
}
赫夫曼树
- 给定n个权值作为n个叶子结点,构造一颗二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree),也翻译为霍夫曼树。
- 赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近。
赫夫曼树的几个重要概念:
-
路径:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。
-
路径长度:通路中分支的数目称为路径长度。若规定根结点的层数为1,则从根结点到第L层结点的路径长度为L-1
-
结点的权:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。
-
带权路径长度:从根结点到该结点之间的路径长度与该结点的权的乘积。
以结点13为例,该结点的路径长度为3-1=2;该结点的带权路径长度为:13*2=26。
-
树的带权路径长度:所有叶子结点的带权路径长度之和,记为WPL(weighted path length),权值越大的结点离根结点越近的二叉树才是最优二叉树。
-
WPL最小的就是赫夫曼树。
构建赫夫曼树
步骤:
- 从小到大进行排序,每一个数据都是一个结点,每个结点可以看成是一个最简单的二叉树;
- 取出根结点权值最小的两棵二叉树;
- 组成一棵新的二叉树,该新的二叉树的根结点的权值是前面两棵二叉树根结点权值的和;
- 再将这棵新的二叉树,以根结点的权值大小再次排序,不断重复1-2-3-4的步骤,直到数列中所有的数据都被处理,就得到一颗赫夫曼树。
package huffmantree;
import sun.font.GlyphLayout;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class HuffmanTree {
public static void main(String[] args) {
int[] arr = {13, 7, 8, 3, 29, 6, 1};
Node root = createHuffmanTree(arr);
preOrder(root);
}
//编写一个前序遍历的方法
public static void preOrder(Node root){
if (root != null){
root.preOrder();
}
else{
System.out.println("树是空树,无法遍历");
}
}
//创建赫夫曼树
/**
*
* @param arr 需要创建赫夫曼树的数组
* @return 创建好后的赫夫曼树的root结点
*/
public static Node createHuffmanTree(int[] arr) {
//为了操作方便
//1.遍历arr数组
//2.将arr的每个元素构成一个Node
//3.将Node放入到ArrayList中
List<Node> nodes = new ArrayList<Node>();
for (int value : arr) {
nodes.add(new Node(value));
}
//我们处理过程是一个循环过程
while (nodes.size() > 1){
//排序 从小到大
Collections.sort(nodes);
//取出根结点权值最小的两棵二叉树
//(1)取出权值最小的结点(二叉树)
Node leftNode = nodes.get(0);
//(2)取出权值第二小的结点(二叉树)
Node rightNode = nodes.get(1);
//(3)构建一棵新的二叉树
Node parent = new Node(leftNode.value+rightNode.value);
parent.left = leftNode;
parent.right = rightNode;
//(4)从ArrayList删除处理过的二叉树
nodes.remove(leftNode);
nodes.remove(rightNode);
//(5)将parent加入到nodes
nodes.add(parent);
}
return nodes.get(0);
}
}
//创建结点类
//为了让Node对象支持排序 Collection集合排序
//让Node实现Comparable接口
class Node implements Comparable<Node>{
int value; // 结点权值
Node left; // 指向左子结点
Node right; // 指向右子结点
//写一个前序遍历
public void preOrder(){
System.out.println(this);
if(this.left != null){
this.left.preOrder();
}
if (this.right != null){
this.right.preOrder();
}
}
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;
}
}
赫夫曼编码
-
传输的字符串:i like like like java do you like a java
-
统计各个字符对应的个数:
d:1;y:1;u:1;j:2;v:2;o:2;l:4;k:4;e:4;i:5;a:5; :9
-
按照上面字符出现的次数构建一棵赫夫曼树,次数作为权值
-
根据赫夫曼树,给各个字符规定编码(前缀编码),向左的路径为0,向右的路径为1,编码如下:
o:1000;u:10010;d:100110;y:100111;i:101;a:110;k:1110;e:1111;j:0000;v:0001;l:001; :01
按照此赫夫曼编码,对字符串进行编码。
注意:根据排序方法不同,赫夫曼树构建形式可能不同(例如,当数组出现多个元素权值相同,每个权值的相对位置由排序算法稳定性决定),这样对应的赫夫曼编码也不完全一样,但是WPL是一样的,都是最小的。
例:对字符串"i like like like java do you like a java"实现赫夫曼编码:
思路:
-
创建字符串对应的赫夫曼树
(1) Node{data(存放数据),weight(权值),left,right}
(2) 得到 "i like like like java do you like a java" 对应的 byte[] 数组
(3) 编写一个方法,将准备构建赫夫曼树的Node结点放到 List ,形式[Node[data=97, weight=5], Node[data=32, weight=9], ......],体现 d:1;y:1;u:1;j:2;v:2;o:2;l:4;k:4;e:4;i:5;a:5; :9
(4) 通过List创建对应的赫夫曼树
package huffmancode;
import java.util.*;
public class HuffmanCode {
public static void main(String[] args) {
String str = "i like like like java do you like a java";
byte[] contentBytes = str.getBytes();
System.out.println(contentBytes.length); //40
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes=" + nodes);
//测试一把创建的二叉树
System.out.println("赫夫曼树");
Node huffmanTreeRoot = createHuffmanTree(nodes);
System.out.println("前序遍历");
preOrder(huffmanTreeRoot);
}
//前序遍历
private static void preOrder(Node root) {
if (root != null){
root.preOrder();
}
else{
System.out.println("赫夫曼树为空");
}
}
/**
*
* @param bytes 接收字节数组
* @return 返回的就是List,形式[Node[data=97, weight=5], Node[data=32, weight=9], ......]
*/
public static List<Node> getNodes(byte[] bytes) {
//1.创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
//遍历bytes,统计每一个byte出现的次数->map[key,value]
Map<Byte,Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) { //Map还没有这个字符数据,第一次
counts.put(b,1);
}
else {
counts.put(b,count+1);
}
}
//每把一个键值对转成一个Node对象,并加入到nodes集合
//遍历map
for (Map.Entry<Byte,Integer> entry: counts.entrySet()) {
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
//可以通过List 创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
//排序
Collections.sort(nodes);
//取出最小的一棵二叉树
Node leftNode = nodes.get(0);
//取出第二小的一棵二叉树
Node rightNode = nodes.get(1);
//创建一棵新的二叉树,它的根结点没有data,只有权值
Node parent = new Node(null,leftNode.weight+rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//将已经处理的两棵二叉树从nodes删除
nodes.remove(leftNode);
nodes.remove(rightNode);
//将新的二叉树加入到nodes
nodes.add(parent);
}
return nodes.get(0);
}
}
//创建Node,带数据和权值
class Node implements Comparable<Node> {
Byte data; // 存放字符本身,比如'a'=>97
Integer weight; // 权值,表示字符出现的次数
Node left;
Node right;
public Node(Byte data, Integer weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
//前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
-
生成赫夫曼树对应的赫夫曼编码表
//生成赫夫曼树对应的赫夫曼编码 //思路: //1. 将赫夫曼编码表存放在 Map<Byte,String>,形式:32->01 97->100 100->11000 ... static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>(); //2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径 static StringBuilder stringBuilder = new StringBuilder(); /** * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合 * @param node 传入结点 * @param code 路径:左子结点是0 右子结点是1 * @param stringBuilder 用于拼接路径 */ private static void getCodes(Node node, String code, StringBuilder stringBuilder) { StringBuilder stringBuilder1 = new StringBuilder(stringBuilder); //将code加入到stringBuilder2 stringBuilder1.append(code); if (node != null) { //如果node==null 不处理 //判断当前node是叶子结点还是非叶子结点 if (node.data == null) { //递归处理 //向左递归 getCodes(node.left,"0",stringBuilder1); //向右递归 getCodes(node.right,"1",stringBuilder1); } else { //就表示找到某个叶子结点 huffmanCodes.put(node.data, stringBuilder1.toString()); } } }
-
字符串生成对应赫夫曼编码
-
huffman编码完整代码
package huffmancode; import java.util.*; public class HuffmanCode { public static void main(String[] args) { String str = "i like like like java do you like a java"; byte[] contentBytes = str.getBytes(); System.out.println(contentBytes.length); //40 byte[] huffmanCodesBytes = huffmanZip(contentBytes); System.out.println("压缩后的结果是"+Arrays.toString(huffmanCodesBytes)); //分布过程 /* List<Node> nodes = getNodes(contentBytes); System.out.println("nodes=" + nodes); //测试一把创建huffman二叉树 System.out.println("赫夫曼树"); Node huffmanTreeRoot = createHuffmanTree(nodes); System.out.println("前序遍历"); preOrder(huffmanTreeRoot); //测试一把是否生成了哈夫曼编码 Map<Byte,String> huffmanCodes = getCodes(huffmanTreeRoot); //System.out.println("生成的哈夫曼编码表"+huffmanCodes); //测试 byte[] huffmanCodesBytes = zip(contentBytes,huffmanCodes); System.out.println("huffmanCodesBytes="+Arrays.toString(huffmanCodesBytes)); */ } //使用一个方法,将前面的方法封装起来,便于我们调用 /** * * @param bytes 原始字符串对应的字节数组 * @return 返回的是经过huffman编码处理后的字节数组 */ private static byte[] huffmanZip(byte[] bytes) { List<Node> nodes = getNodes(bytes); //根据创建的nodes 创建huffman二叉树 Node huffmanTreeRoot = createHuffmanTree(nodes); //根据huffman树,生成哈夫曼编码 Map<Byte,String> huffmanCodes = getCodes(huffmanTreeRoot); //根据huffman编码,得到压缩后的huffman编码压缩数组 byte[] huffmanCodesBytes = zip(bytes,huffmanCodes); return huffmanCodesBytes; } //编写一个方法,通过生成的huffman编码表,将一个字符串对应的byte[] 返回压缩码 /** * * @param bytes 原始字符串对应的byte[] * @param huffmanCodes 生成的赫夫曼编码map * @return 返回赫夫曼编码处理后的byte[] * 举例:"i like like like java do you like a java" 的byte[] ===> "10101000...."的byte[],即 * huffmanCodeByte[0]= 10101000(补码) ==> byte [推导 10101000 => 10101000-1 => 10100111(反码) => 11011000=-88] * huffmanCodeByte[0]=-88 */ private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) { //1.利用huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串 StringBuilder stringBuilder = new StringBuilder(); //遍历bytes 数组 for (byte b : bytes) { stringBuilder.append(huffmanCodes.get(b)); } //将赫夫曼编码字符串"101010001011111..." 转成 byte[] //统计返回 byte[] huffmanCodeByte 长度 //一句话 int len = (stringBuilder.length() + 7) / 8; int len; if (stringBuilder.length() % 8 == 0) { len = stringBuilder.length()/8; } else { len = stringBuilder.length()/8 + 1; } //创建存储压缩后的byte[] byte[] huffmanCodeByte = new byte[len]; int index = 0; //记录是第几个byte for (int i = 0; i < stringBuilder.length(); i+=8) {//因为是每8位对应一个byte,所以步长+8 String strByte; if (i+8>stringBuilder.length()){ //不够8位 strByte = stringBuilder.substring(i); } else { strByte = stringBuilder.substring(i,i+8); } //将strByte转成一个byte,放入到huffmanCodeByte huffmanCodeByte[index] = (byte)Integer.parseInt(strByte,2); index++; } return huffmanCodeByte; } //生成赫夫曼树对应的赫夫曼编码 //思路: //1. 将赫夫曼编码表存放在 Map<Byte,String>,形式:32->01 97->100 100->11000 ... static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>(); //2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径 static StringBuilder stringBuilder = new StringBuilder(); //为了调用方便,重载getCodes方法 private static Map<Byte,String> getCodes(Node root) { if (root == null) { return null; } //处理root的左子树 getCodes(root.left,"0",stringBuilder); //处理root的右子树 getCodes(root.right,"1",stringBuilder); return huffmanCodes; } /** * 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合 * @param node 传入结点 * @param code 路径:左子结点是0 右子结点是1 * @param stringBuilder 用于拼接路径 */ private static void getCodes(Node node, String code, StringBuilder stringBuilder) { StringBuilder stringBuilder1 = new StringBuilder(stringBuilder); //将code加入到stringBuilder2 stringBuilder1.append(code); if (node != null) { //如果node==null 不处理 //判断当前node是叶子结点还是非叶子结点 if (node.data == null) { //递归处理 //向左递归 getCodes(node.left,"0",stringBuilder1); //向右递归 getCodes(node.right,"1",stringBuilder1); } else { //就表示找到某个叶子结点 huffmanCodes.put(node.data, stringBuilder1.toString()); } } } //前序遍历 private static void preOrder(Node root) { if (root != null){ root.preOrder(); } else{ System.out.println("赫夫曼树为空"); } } /** * * @param bytes 接收字节数组 * @return 返回的就是List,形式[Node[data=97, weight=5], Node[data=32, weight=9], ......] */ public static List<Node> getNodes(byte[] bytes) { //1.创建一个ArrayList ArrayList<Node> nodes = new ArrayList<Node>(); //遍历bytes,统计每一个byte出现的次数->map[key,value] Map<Byte,Integer> counts = new HashMap<>(); for (byte b : bytes) { Integer count = counts.get(b); if (count == null) { //Map还没有这个字符数据,第一次 counts.put(b,1); } else { counts.put(b,count+1); } } //每把一个键值对转成一个Node对象,并加入到nodes集合 //遍历map for (Map.Entry<Byte,Integer> entry: counts.entrySet()) { nodes.add(new Node(entry.getKey(),entry.getValue())); } return nodes; } //可以通过List 创建对应的赫夫曼树 private static Node createHuffmanTree(List<Node> nodes) { while (nodes.size() > 1) { //排序 Collections.sort(nodes); //取出最小的一棵二叉树 Node leftNode = nodes.get(0); //取出第二小的一棵二叉树 Node rightNode = nodes.get(1); //创建一棵新的二叉树,它的根结点没有data,只有权值 Node parent = new Node(null,leftNode.weight+rightNode.weight); parent.left = leftNode; parent.right = rightNode; //将已经处理的两棵二叉树从nodes删除 nodes.remove(leftNode); nodes.remove(rightNode); //将新的二叉树加入到nodes nodes.add(parent); } return nodes.get(0); } } //创建Node,带数据和权值 class Node implements Comparable<Node> { Byte data; // 存放字符本身,比如'a'=>97 Integer weight; // 权值,表示字符出现的次数 Node left; Node right; public Node(Byte data, Integer weight) { this.data = data; this.weight = weight; } @Override public int compareTo(Node o) { return this.weight - o.weight; } @Override public String toString() { return "Node{" + "data=" + data + ", weight=" + weight + '}'; } //前序遍历 public void preOrder() { System.out.println(this); if (this.left != null) { this.left.preOrder(); } if (this.right != null) { this.right.preOrder(); } } }
赫夫曼解码
package huffmancode;
import java.util.*;
public class HuffmanCode {
public static void main(String[] args) {
String str = "i like like like java do you like a java";
byte[] contentBytes = str.getBytes();
System.out.println(contentBytes.length); //40
byte[] huffmanCodesBytes = huffmanZip(contentBytes);
System.out.println("压缩后的结果是"+Arrays.toString(huffmanCodesBytes));
byte[] sourceBytes = decode(huffmanCodes,huffmanCodesBytes);
System.out.println("原来的字符串=" + new String(sourceBytes));
//分布过程
/*
List<Node> nodes = getNodes(contentBytes);
System.out.println("nodes=" + nodes);
//测试一把创建huffman二叉树
System.out.println("赫夫曼树");
Node huffmanTreeRoot = createHuffmanTree(nodes);
System.out.println("前序遍历");
preOrder(huffmanTreeRoot);
//测试一把是否生成了哈夫曼编码
Map<Byte,String> huffmanCodes = getCodes(huffmanTreeRoot);
//System.out.println("生成的哈夫曼编码表"+huffmanCodes);
//测试
byte[] huffmanCodesBytes = zip(contentBytes,huffmanCodes);
System.out.println("huffmanCodesBytes="+Arrays.toString(huffmanCodesBytes));
*/
}
//完成数据的解压
//思路
//1.将huffmanCodeBytes数组 重新转成Huffman编码对应的二进制字符串
//2.将Huffman编码对应的二进制字符串 转成原始字符串
//编写一个方法,完成对压缩数据的解码
/**
*
* @param huffmanCodes 赫夫曼编码表
* @param huffmanBytes 赫夫曼编码得到的字节数组
* @return 原来的字符串对应数组
*/
private static byte[] decode(Map<Byte,String> huffmanCodes, byte[] huffmanBytes) {
//1.先得到 huffmanBytes 对应的二进制的字符串
StringBuilder stringBuilder = new StringBuilder();
//将byte数组转成二进制的字符串
for (int i = 0; i < huffmanBytes.length; i++) {
byte b = huffmanBytes[i];
//判断是不是最后一个字节
boolean flag = (i == huffmanBytes.length-1);
stringBuilder.append(byteToBitString(!flag,b));
}
//把字符串按照指定的赫夫曼编码表进行解码
//把赫夫曼编码表进行调换,因为反向查询a->100 100->?
Map<String,Byte> map = new HashMap<String,Byte>();
for (Map.Entry<Byte,String> entry: huffmanCodes.entrySet()) {
map.put(entry.getValue(),entry.getKey());
}
//创建一个集合,存放byte
List<Byte> list = new ArrayList<>();
//i 可以理解为就是索引,扫描stringBuilder
for (int i=0; i<stringBuilder.length();) {
int count = 1; //小的计数器
boolean flag = true;
Byte b = null;
while (flag) {
//递增取出'1'/'0'
String key = stringBuilder.substring(i,i+count); //i不动,让count移动,直到匹配到一个字符
b = map.get(key);
if (b == null) {//说明没有匹配到
count++;
}
else{ //匹配到
flag=false;
}
}
list.add(b);
i += count; //i 移动到count
}
//当for循环结束后,list中就存放了所有原始字符串
//把list中的数据放入到byte[] 并返回
byte[] b = new byte[list.size()];
for (int i = 0; i < b.length; i++) {
b[i] = list.get(i);
}
return b;
}
/**
* 将一个byte转成一个二进制的字符串
* @param b 传入的byte
* @param flag 标志是否需要补高位,如果是true表示需要补,如果是false表示不需要补
* @return 是该b 对应的二进制字符串,(注意是按补码返回)
*/
private static String byteToBitString(boolean flag, byte b) {
//使用变量保存b
int temp = b; //将 b 转成int
//如果是正数,我们还存在补高位的问题
if (flag) {
temp |= 256; //按位或 1 0000 0000
}
String str = Integer.toBinaryString(temp); //返回的是temp对应的二进制补码
if (flag) {
return str.substring(str.length()-8);
}
else {
return str;
}
}
//使用一个方法,将前面的方法封装起来,便于我们调用
/**
*
* @param bytes 原始字符串对应的字节数组
* @return 返回的是经过huffman编码处理后的字节数组
*/
private static byte[] huffmanZip(byte[] bytes) {
List<Node> nodes = getNodes(bytes);
//根据创建的nodes 创建huffman二叉树
Node huffmanTreeRoot = createHuffmanTree(nodes);
//根据huffman树,生成哈夫曼编码
Map<Byte,String> huffmanCodes = getCodes(huffmanTreeRoot);
//根据huffman编码,得到压缩后的huffman编码压缩数组
byte[] huffmanCodesBytes = zip(bytes,huffmanCodes);
return huffmanCodesBytes;
}
//编写一个方法,通过生成的huffman编码表,将一个字符串对应的byte[] 返回压缩码
/**
*
* @param bytes 原始字符串对应的byte[]
* @param huffmanCodes 生成的赫夫曼编码map
* @return 返回赫夫曼编码处理后的byte[]
* 举例:"i like like like java do you like a java" 的byte[] ===> "10101000...."的byte[],即
* huffmanCodeByte[0]= 10101000(补码) ==> byte [推导 10101000 => 10101000-1 => 10100111(反码) => 11011000=-88]
* huffmanCodeByte[0]=-88
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
//1.利用huffmanCodes 将 bytes 转成赫夫曼编码对应的字符串
StringBuilder stringBuilder = new StringBuilder();
//遍历bytes 数组
for (byte b : bytes) {
stringBuilder.append(huffmanCodes.get(b));
}
//将赫夫曼编码字符串"101010001011111..." 转成 byte[]
//统计返回 byte[] huffmanCodeByte 长度
//一句话 int len = (stringBuilder.length() + 7) / 8;
int len;
if (stringBuilder.length() % 8 == 0) {
len = stringBuilder.length()/8;
}
else {
len = stringBuilder.length()/8 + 1;
}
//创建存储压缩后的byte[]
byte[] huffmanCodeByte = new byte[len];
int index = 0; //记录是第几个byte
for (int i = 0; i < stringBuilder.length(); i+=8) {//因为是每8位对应一个byte,所以步长+8
String strByte;
if (i+8>stringBuilder.length()){ //不够8位
strByte = stringBuilder.substring(i);
}
else {
strByte = stringBuilder.substring(i,i+8);
}
//将strByte转成一个byte,放入到huffmanCodeByte
huffmanCodeByte[index] = (byte)Integer.parseInt(strByte,2);
index++;
}
return huffmanCodeByte;
}
//生成赫夫曼树对应的赫夫曼编码
//思路:
//1. 将赫夫曼编码表存放在 Map<Byte,String>,形式:32->01 97->100 100->11000 ...
static Map<Byte,String> huffmanCodes = new HashMap<Byte,String>();
//2.在生成赫夫曼编码表时,需要去拼接路径,定义一个StringBuilder 存储某个叶子结点的路径
static StringBuilder stringBuilder = new StringBuilder();
//为了调用方便,重载getCodes方法
private static Map<Byte,String> getCodes(Node root) {
if (root == null) {
return null;
}
//处理root的左子树
getCodes(root.left,"0",stringBuilder);
//处理root的右子树
getCodes(root.right,"1",stringBuilder);
return huffmanCodes;
}
/**
* 功能:将传入的node结点的所有叶子结点的赫夫曼编码得到,并放入到huffmanCodes集合
* @param node 传入结点
* @param code 路径:左子结点是0 右子结点是1
* @param stringBuilder 用于拼接路径
*/
private static void getCodes(Node node, String code, StringBuilder stringBuilder) {
StringBuilder stringBuilder1 = new StringBuilder(stringBuilder);
//将code加入到stringBuilder2
stringBuilder1.append(code);
if (node != null) { //如果node==null 不处理
//判断当前node是叶子结点还是非叶子结点
if (node.data == null) {
//递归处理
//向左递归
getCodes(node.left,"0",stringBuilder1);
//向右递归
getCodes(node.right,"1",stringBuilder1);
}
else {
//就表示找到某个叶子结点
huffmanCodes.put(node.data, stringBuilder1.toString());
}
}
}
//前序遍历
private static void preOrder(Node root) {
if (root != null){
root.preOrder();
}
else{
System.out.println("赫夫曼树为空");
}
}
/**
*
* @param bytes 接收字节数组
* @return 返回的就是List,形式[Node[data=97, weight=5], Node[data=32, weight=9], ......]
*/
public static List<Node> getNodes(byte[] bytes) {
//1.创建一个ArrayList
ArrayList<Node> nodes = new ArrayList<Node>();
//遍历bytes,统计每一个byte出现的次数->map[key,value]
Map<Byte,Integer> counts = new HashMap<>();
for (byte b : bytes) {
Integer count = counts.get(b);
if (count == null) { //Map还没有这个字符数据,第一次
counts.put(b,1);
}
else {
counts.put(b,count+1);
}
}
//每把一个键值对转成一个Node对象,并加入到nodes集合
//遍历map
for (Map.Entry<Byte,Integer> entry: counts.entrySet()) {
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
//可以通过List 创建对应的赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size() > 1) {
//排序
Collections.sort(nodes);
//取出最小的一棵二叉树
Node leftNode = nodes.get(0);
//取出第二小的一棵二叉树
Node rightNode = nodes.get(1);
//创建一棵新的二叉树,它的根结点没有data,只有权值
Node parent = new Node(null,leftNode.weight+rightNode.weight);
parent.left = leftNode;
parent.right = rightNode;
//将已经处理的两棵二叉树从nodes删除
nodes.remove(leftNode);
nodes.remove(rightNode);
//将新的二叉树加入到nodes
nodes.add(parent);
}
return nodes.get(0);
}
}
//创建Node,带数据和权值
class Node implements Comparable<Node> {
Byte data; // 存放字符本身,比如'a'=>97
Integer weight; // 权值,表示字符出现的次数
Node left;
Node right;
public Node(Byte data, Integer weight) {
this.data = data;
this.weight = weight;
}
@Override
public int compareTo(Node o) {
return this.weight - o.weight;
}
@Override
public String toString() {
return "Node{" +
"data=" + data +
", weight=" + weight +
'}';
}
//前序遍历
public void preOrder() {
System.out.println(this);
if (this.left != null) {
this.left.preOrder();
}
if (this.right != null) {
this.right.preOrder();
}
}
}
二叉排序树(BST树)
二叉排序树:BST(Binary Sort(Search) Tree),对于二叉排序树的任何一个非叶子结点,要求左子结点的值比当前结点的值小,右子结点的值比当前结点的值大。特别说明:如果有相同的值,可以将该结点放在左子结点或右子结点。
二叉排序树的创建和遍历
例:对数组arr[7,3,10,12,5,1,9]创建二叉排序树,使用中序遍历二叉排序树。
package binarysorttree;
public class BinarySortTressDemo {
public static void main(String[] args) {
int[] arr={7,3,10,12,5,1,9};
BinarySortTree binarySortTree = new BinarySortTree();
//循环的添加结点到二叉排序树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//遍历二叉排序树
System.out.println("中序遍历二叉排序树");
binarySortTree.infixOrder();
}
}
//创建二叉排序树
class BinarySortTree {
private Node root;
//添加结点的方法
public void add(Node node) {
if (root == null) {
root = node; //如果root为空,则root直接指向node
}
else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
}
else {
System.out.println("二叉排序树为空,不能遍历");
}
}
}
//创建Node结点
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//添加结点的方法
//递归的形式添加结点,注意需要满足二叉排序树的要求
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);
}
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
二叉排序树的删除
二叉排序树的删除情况比较复杂,有下面三种情况需要考虑:
-
删除叶子结点(比如:2,5,9,12)
-
删除只有一棵子树的结点(比如:1)
-
删除有两棵子树的结点(比如:7,3,10)
package binarysorttree;
public class BinarySortTressDemo {
public static void main(String[] args) {
int[] arr={7,3,10,12,5,1,9,2};
BinarySortTree binarySortTree = new BinarySortTree();
//循环的添加结点到二叉排序树
for (int i = 0; i < arr.length; i++) {
binarySortTree.add(new Node(arr[i]));
}
//遍历二叉排序树
System.out.println("中序遍历二叉排序树");
binarySortTree.infixOrder();
//测试删除叶子结点
binarySortTree.delNode(5);
System.out.println("删除结叶子点后");
binarySortTree.infixOrder();
//测试删除只有一个子结点的结点
binarySortTree.delNode(1);
System.out.println("删除只有一个子结点的结点后");
binarySortTree.infixOrder();
//测试删除有两个子结点的结点
binarySortTree.delNode(7);
System.out.println("删除只有一个子结点的结点后");
binarySortTree.infixOrder();
}
}
//创建二叉排序树
class BinarySortTree {
private Node root;
//查找要删除的结点
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);
}
}
//编写方法:
//1.返回以node为根结点的二叉排序树的最小结点的值
//2.删除node为根结点的二叉排序树的最小结点
/**
*
* @param node 传入的结点(当作二叉排序树的根结点)
* @return 返回以node为根结点的二叉排序树的最小结点的值
*/
public int delRightTreeMin(Node node) {
Node target = node;
//循环的查找左节点,就会找到最小值
while (target.left != null) {
target = target.left;
}
//这是target就指向了最小结点,删除最小节点
delNode(target.value);
return target.value;
}
//删除节点
public void delNode(int value) {
if (root == null) {
return;
}
else {
//1.需要先找到要删除的结点 targetNode
Node targetNode = search(value);
//如果没有找到要删除的结点
if (targetNode == null) {
return;
}
//如果我们发现当前这棵二叉排序树只有一个结点
if (root.left == null && root.right == null) {
root = null;
return;
}
//去查找targetNode的父结点
Node parent = searchParent(value);
//如果要删除的结点是叶子结点
if (targetNode.left==null && targetNode.right==null) {
//判断targetNode 是父结点的左子结点还是右子结点
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) {
int minVal = delRightTreeMin(targetNode.right);
targetNode.value=minVal;
}
else { //删除只有一棵子树的结点
//如果要删除的结点有左子结点
if (targetNode.left != null) {
if (parent != null) {
//如果targetNode是parent的左子结点
if (parent.left.value==value) {
parent.left=targetNode.left;
}
else {//如果targetNode是parent的右子结点
parent.right=targetNode.left;
}
}
else {
root=targetNode.left;
}
}
else { //如果要删除的结点有右子结点
if (parent != null) {
//如果targetNode是parent的左子结点
if (parent.left.value==value) {
parent.left=targetNode.right;
}
else {//如果targetNode是parent的右子结点
parent.right=targetNode.right;
}
}
else {
root = targetNode.right;
}
}
}
}
}
//添加结点的方法
public void add(Node node) {
if (root == null) {
root = node; //如果root为空,则root直接指向node
}
else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
}
else {
System.out.println("二叉排序树为空,不能遍历");
}
}
}
//创建Node结点
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
//查找要删除的结点
/**
*
* @param value 希望删除的结点的值
* @return 如果找到返回该结点,否则返回null
*/
public Node search(int value) {
if (value == this.value) { //找到该结点
return this;
}
else if (value < this.value) {//如果查找的值,小于当前结点,应该向左子树递归查找
//如果左子结点为空,找不到
if (this.left == null) {
return null;
}
return this.left.search(value);
}
else {//如果查找的值,不小于当前结点,应该向左子树递归查找
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
//查找要删除结点的父结点
/**
*
* @param value 要查找的结点的值
* @return 返回的是要删除结点的父结点,如果没有,返回null
*/
public Node searchParent(int value) {
//如果当前结点就是要删除结点的父结点,就返回
if ((this.left != null && this.left.value==value) || (this.right != null && this.right.value==value)) {
return this;
}
else {
//如果查找的值小于当前结点的值,并且当前结点的左子结点不为空
if (value < this.value && this.left != null) {
return this.left.searchParent(value); //向左子树递归查找
}
else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); //向右子树递归查找
}
else {
return null; //没有找到父结点
}
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//添加结点的方法
//递归的形式添加结点,注意需要满足二叉排序树的要求
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);
}
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
平衡二叉树(AVL树)
- 平衡二叉树又叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树。
- 特点:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等。
左旋转
右旋转
双旋转
package avl;
public class AVLTreeDemo {
public static void main(String[] args) {
//int[] arr = {4,3,6,5,7,8}; //左旋测试
//int[] arr = {10,12,8,9,7,6}; //右旋测试
int[] arr = {10,11,7,6,8,9}; //测试双旋转
//创建一个AVLTree对象
AVLTree avlTree = new AVLTree();
//添加结点
for (int i = 0; i < arr.length; i++) {
avlTree.add(new Node(arr[i]));
}
//遍历
System.out.println("中序遍历");
avlTree.infixOrder();
System.out.println("在没有做平衡处理前~~");
System.out.println("树的高度="+avlTree.getRoot().height());
System.out.println("树的左子树高度="+avlTree.getRoot().leftHeight());
System.out.println("树的右子树高度="+avlTree.getRoot().rightHeight());
}
}
//创建AVL树
class AVLTree {
private Node root;
public Node getRoot() {
return root;
}
//查找要删除的结点
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);
}
}
//编写方法:
//1.返回以node为根结点的二叉排序树的最小结点的值
//2.删除node为根结点的二叉排序树的最小结点
/**
*
* @param node 传入的结点(当作二叉排序树的根结点)
* @return 返回以node为根结点的二叉排序树的最小结点的值
*/
public int delRightTreeMin(Node node) {
Node target = node;
//循环的查找左节点,就会找到最小值
while (target.left != null) {
target = target.left;
}
//这是target就指向了最小结点,删除最小节点
delNode(target.value);
return target.value;
}
//删除节点
public void delNode(int value) {
if (root == null) {
return;
}
else {
//1.需要先找到要删除的结点 targetNode
Node targetNode = search(value);
//如果没有找到要删除的结点
if (targetNode == null) {
return;
}
//如果我们发现当前这棵二叉排序树只有一个结点
if (root.left == null && root.right == null) {
root = null;
return;
}
//去查找targetNode的父结点
Node parent = searchParent(value);
//如果要删除的结点是叶子结点
if (targetNode.left==null && targetNode.right==null) {
//判断targetNode 是父结点的左子结点还是右子结点
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) {
int minVal = delRightTreeMin(targetNode.right);
targetNode.value=minVal;
}
else { //删除只有一棵子树的结点
//如果要删除的结点有左子结点
if (targetNode.left != null) {
if (parent != null) {
//如果targetNode是parent的左子结点
if (parent.left.value==value) {
parent.left=targetNode.left;
}
else {//如果targetNode是parent的右子结点
parent.right=targetNode.left;
}
}
else {
root=targetNode.left;
}
}
else { //如果要删除的结点有右子结点
if (parent != null) {
//如果targetNode是parent的左子结点
if (parent.left.value==value) {
parent.left=targetNode.right;
}
else {//如果targetNode是parent的右子结点
parent.right=targetNode.right;
}
}
else {
root = targetNode.right;
}
}
}
}
}
//添加结点的方法
public void add(Node node) {
if (root == null) {
root = node; //如果root为空,则root直接指向node
}
else {
root.add(node);
}
}
//中序遍历
public void infixOrder() {
if (root != null) {
root.infixOrder();
}
else {
System.out.println("二叉排序树为空,不能遍历");
}
}
}
//创建Node结点
class Node {
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
//返回左子树的高度
public int leftHeight() {
if (left == null) {
return 0;
}
return left.height();
}
//返回右子树的高度
public int rightHeight() {
if (right == null) {
return 0;
}
return right.height();
}
//返回以该结点为根结点的树的高度
public int height() {
return Math.max(left==null?0:left.height(),right==null?0: right.height())+1;
}
//左旋转的方法
private void leftRotate() {
//创建新的结点,以当前结点的值
Node newNode = new Node(value);
//把新结点的左子树设置成当前根结点的左子树
newNode.left = left;
//把新的结点的右子树设置成当前结点右子树的左子树
newNode.right = right.left;
//把当前结点的值替换成右子结点的值
value = right.value;
//把当前结点的右子树设置成当前结点右子树的右子树
right = right.right;
//把当前结点的左子树(左子结点)设置成新的结点
left = newNode;
}
//右旋转
private void rightRotate() {
Node newNode = new Node(value);
newNode.right = right;
newNode.left = left.right;
value = left.value;
left = left.left;
right = newNode;
}
//查找要删除的结点
/**
*
* @param value 希望删除的结点的值
* @return 如果找到返回该结点,否则返回null
*/
public Node search(int value) {
if (value == this.value) { //找到该结点
return this;
}
else if (value < this.value) {//如果查找的值,小于当前结点,应该向左子树递归查找
//如果左子结点为空,找不到
if (this.left == null) {
return null;
}
return this.left.search(value);
}
else {//如果查找的值,不小于当前结点,应该向左子树递归查找
if (this.right == null) {
return null;
}
return this.right.search(value);
}
}
//查找要删除结点的父结点
/**
*
* @param value 要查找的结点的值
* @return 返回的是要删除结点的父结点,如果没有,返回null
*/
public Node searchParent(int value) {
//如果当前结点就是要删除结点的父结点,就返回
if ((this.left != null && this.left.value==value) || (this.right != null && this.right.value==value)) {
return this;
}
else {
//如果查找的值小于当前结点的值,并且当前结点的左子结点不为空
if (value < this.value && this.left != null) {
return this.left.searchParent(value); //向左子树递归查找
}
else if (value >= this.value && this.right != null) {
return this.right.searchParent(value); //向右子树递归查找
}
else {
return null; //没有找到父结点
}
}
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
//添加结点的方法
//递归的形式添加结点,注意需要满足二叉排序树的要求
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();
}
}
}
//中序遍历
public void infixOrder() {
if (this.left != null) {
this.left.infixOrder();
}
System.out.println(this);
if (this.right != null) {
this.right.infixOrder();
}
}
}
多叉树
- 如果允许每个节点可以有更多的数据项和更多的子结点,就是多叉树(multiway tree)。
- 2-3树、2-3-4树就是多叉树,多叉树通过重新组织结点,减少树的高度,能对二叉树进行优化。
2-3树
2-3树是最简单的B树结构,具有如下特点:
- 2-3树的所有叶子结点都在同一层。(只要是B树都满足这个条件)
- 有两个子结点的结点叫二结点,二结点要么没有子结点,要么有两个子结点。
- 有三个子结点的结点叫三结点,三结点要么没有子结点,要么有三个子结点。
- 2-3树是由二结点和三结点构成的树。
B树
B-Tree即B树,B即Balanced,平衡的意思。2-3树和2-3-4树都是B树。
- B树的阶:结点的最多子结点个数。比如2-3树的阶是3,2-3-4树的阶是4。
- B树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点
- 关键字集合分布在整棵树中,即叶子结点和非叶子结点都存放数据
- 搜索有可能在非叶子结点结束
- 其搜索性能等价于在关键字全集内做一次二分查找
B+树
B+树是B树的变体。
- 所有真实数据都存放在叶子结点的链表中(即数据只能在叶子结点,也叫稠密索引),且链表中的数据有序。
- 非叶子结点相当于叶子结点的索引(稀疏索引)
- 更适合文件索引系统。
B*树
B*树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。
图
图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边,结点也可以称为顶点。
- 顶点(vertex)
- 边(edge)
- 路径
- 无向图
- 有向图
- 带权图
图的表示方式
图的表示方式有两种:二维数组表示(邻接矩阵);链表表示(邻接表)。
邻接矩阵
邻接矩阵是表示图形中顶点之间相邻关系的矩阵,对于n个顶点的图而言,矩阵的row和clo表示的是1...n个点。
无向图的邻接矩阵是对称矩阵。
邻接表
- 邻接矩阵需要为每个顶点都分配n个边的空间,其实有很多边都是不存在,会造成空间的一定损失。
- 邻接表的实现只关心存在的边,不关心不存在的边。因此没有空间浪费,邻接表由数组+链表组成。
图的创建
package graph;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
public class Graph {
private ArrayList<String> vertexList; //存储顶点的集合
private int[][] edges; //存储图对应的邻接矩阵
private int numOfEdges; //表示边的数目
public static void main(String[] args) {
//测试图是否创建OK
int n = 5; //结点的个数
String[] vertexs = {"A","B","C","D","E"};
//创建图对象
Graph graph = new Graph(n);
//循环的添加顶点
for (String vertex : vertexs) {
graph.insertVertex(vertex);
}
//添加边
//A-B A-C B-C B-D B-E
graph.insertEdge(0,1,1);
graph.insertEdge(0,2,1);
graph.insertEdge(1,2,1);
graph.insertEdge(1,3,1);
graph.insertEdge(1,4,1);
//显示邻接矩阵
graph.showGraph();
}
//构造器
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
}
//图中常用的方法:
//显示图对应的矩阵
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
//返回顶点的个数
public int getNumOgVertex() {
return vertexList.size();
}
//得到边的数目
public int getNumOfEdges() {
return numOfEdges;
}
//返回顶点i(下标)对应的数据
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 表示第一个点的下标,即是第几个顶点
* @param v2 第二个顶点对应的下标
* @param weight 表示是否相连 0/1
*/
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
}
图的深度优先遍历
- 深度优先遍历(Depth First Search),从初始访问节点出发,初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接结点,可以这样理解:每次都在访问完当前结点后首先访问当前结点的第一个邻接结点。
- 我们可以看到,这样的访问策略是优先往纵向挖掘深入,而不是对一个结点的所有邻接结点进行横向访问。
- 深度优先搜索是一个递归的过程。
算法步骤:
- 访问初始结点v,并标记结点v为已访问
- 查找结点v的第一个邻接结点w
- 若w存在,则继续执行4;如果w不存在,则返回到第1步,将从v的下一个邻接结点继续
- 若w未被访问,对w进行深度优先遍历递归(即把w当作另一个v,然后进行步骤123)
- 若w已被访问,查找结点v的w邻接结点的下一个邻接结点,转到步骤3
图的广度优先遍历
广度优先遍历(Broad First Search)
类似于一个分层搜索的过程,广度优先遍历需要使用一个队列以保持访问过的结点的顺序,以便按这个顺序来访问这些结点的邻接结点。
算法步骤:
- 访问初始结点v并标记v已访问
- 结点v入队列
- 当队列非空时,继续执行,否则算法结束
- 出队列,取得队头结点u
- 查找结点u的第一个邻接结点w
- 若结点u的邻接结点w不存在,则转到步骤3;否则循环执行以下三个步骤:
6.1. 若结点w尚未被访问,则访问结点w并标记已访问;若结点已被访问,执行步骤6.3
6.2. 结点w入队列
6.3. 查找结点u的继w邻接结点后的下一个邻接结点w,转到步骤6
package graph;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
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 = 5; //结点的个数
String[] vertexs = {"A","B","C","D","E"};
//创建图对象
Graph graph = new Graph(n);
//循环的添加顶点
for (String vertex : vertexs) {
graph.insertVertex(vertex);
}
//添加边
//A-B A-C B-C B-D B-E
graph.insertEdge(0,1,1);
graph.insertEdge(0,2,1);
graph.insertEdge(1,2,1);
graph.insertEdge(1,3,1);
graph.insertEdge(1,4,1);
//显示邻接矩阵
graph.showGraph();
//测试dfs
// System.out.println("深度遍历");
// graph.dfs();
System.out.println("广度遍历");
graph.bfs();
}
//构造器
public Graph(int n) {
//初始化矩阵和vertexList
edges = new int[n][n];
vertexList = new ArrayList<String>(n);
numOfEdges = 0;
isVisited = new boolean[n];
}
//得到第一个邻接结点的下标
public int getFirstNeighbor(int index) {
for (int i = 0; i < vertexList.size(); i++) {
if (edges[index][i] > 0) {
return i;
}
}
return -1;
}
//根据前一个邻接结点的下标来获取下一个邻接结点
/**
*
* @param v1 当前结点下标
* @param v2 前一个邻接结点下标
* @return
*/
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;
}
//深度优先遍历算法
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);
}
else { //如果w结点已经被访问过
w = getNextNeighbor(i, w);
}
}
}
//对dfs 进行一个重载 遍历我们所有的结点,并进行dfs
public void dfs() {
//遍历所有的结点,进行dfs
for (int i = 0; i < getNumOgVertex(); i++) {
if (!isVisited[i]) {
dfs(isVisited,i);
}
}
}
//对一个结点进行广度优先遍历
private void bfs(boolean[] isVisited, int i) {
int u; //表示队列的头节点对应的下标
int 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); //体现出广度优先
}
}
}
//遍历所有的结点,都进行BFS
public void bfs() {
for (int i = 0; i < getNumOgVertex(); i++) {
if (!isVisited[i]) {
bfs(isVisited,i);
}
}
}
//图中常用的方法:
//显示图对应的矩阵
public void showGraph() {
for (int[] link : edges) {
System.out.println(Arrays.toString(link));
}
}
//返回顶点的个数
public int getNumOgVertex() {
return vertexList.size();
}
//得到边的数目
public int getNumOfEdges() {
return numOfEdges;
}
//返回顶点i(下标)对应的数据
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 表示第一个点的下标,即是第几个顶点
* @param v2 第二个顶点对应的下标
* @param weight 表示是否相连 0/1
*/
public void insertEdge(int v1, int v2, int weight) {
edges[v1][v2] = weight;
edges[v2][v1] = weight;
numOfEdges++;
}
}
算法
递归
三道题套路解决递归问题 | lyl's blog (lyl0724.github.io)
递归用于解决什么样的问题:
- 各种数学问题:8皇后问题、汉诺塔、阶乘问题、迷宫问题、球和篮子问题(google编程大赛)。
- 各种算法中也会用到递归,比如快排、归并排序、二分查找、分治算法等。
- 将用栈解决的问题--->递归代码比较简洁。
递归需要遵守的重要规则:
- 执行一个方法时,就创建一个新的受保护的独立空间(栈空间);
- 方法的局部变量的独立的,不会相互影响;
- 如果方法中使用的是引用类型的变量(比如数组),就会共享该引用类型的数据;
- 递归必须想退出递归的条件逼近,否则就是无限递归,出现StackOverFlowError;
- 当一个方法执行完毕,或者遇到return,就会返回,遵守“谁调用,结果返回给谁”,同时当方法执行完毕或者返回时,该方法也就执行完毕了。
迷宫问题:
public class MiGong {
public static void main(String[] args){
//先创建一个二维数组,模拟迷宫
//地图
int[][] map = new int[8][7];
//使用1表示墙
//上下全部置为1
for(int i=0; i<7; i++){
map[0][i]=1;
map[7][i]=1;
}
//左右全部置为1
for(int i=0;i<8; i++){
map[i][0]=1;
map[i][6]=1;
}
map[3][1]=1;
map[3][2]=1;
//输出地图
System.out.println("地图情况");
for(int i = 0; i<8;i++){
for(int j =0; j<7;j++){
System.out.print(map[i][j]+" ");
}
System.out.println();
}
//使用递归回溯给小球找路
setWay(map,1,1);
//输出新的地图,小球走过,并标识过的
System.out.println("小球走过并标识过的地图");
for(int i = 0; i<8;i++){
for(int j =0; j<7;j++){
System.out.print(map[i][j]+" ");
}
System.out.println();
}
}
//使用递归回溯来给小球找路
//说明
//1.map表示地图
//2.(i,j)表示从地图开始出发位置,例如(1,1)
//3.如果小球能到map[6][5]位置,则说明通路找到
//4.约定:当map[i][j]为0表示该点没有走过,当为1表示墙,2表示通路可以走,3表示该位置已经走过但是走不通
//5.在走迷宫时,需要确定一个策略(方法):下->右->上->左,如果该点走不通再回溯
/**
*
* @param map 表示地图
* @param i 从第i行开始
* @param j 从第j列开始
* @return 如果找到通路就返回true,否则返回false
*/
public static boolean setWay(int[][] map,int i, int j){
if(map[6][5] == 2){ //通路已经找到
return true;
}else{
if(map[i][j]==0){ //如果当前这个点还没有走过
//按照策略 下->右->上->左 走
map[i][j] = 2; //假定该点可以走通
if(setWay(map,i+1,j)){ //向下走
return true;
}
else if(setWay(map,i,j+1)){ //向右走
return true;
}
else if(setWay(map,i-1,j)) { //向上走
return true;
}
else if(setWay(map,i,j-1)){ //向左走
return true;
}
else{ //说明该点是走不通的,是死路
map[i][j]=3;
return false;
}
}
else{ //如果map[i][j]!=0,map可能是1,2,3
return false;
}
}
}
}
八皇后问题:
思路:
- 第一个皇后先放第一行第一列
- 第二个皇后放在第二行第一列,然后判断是否OK,如果不OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适
- 继续第三个皇后,还是第一列、第二列......直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解
- 当得到一个正确解时,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解全部得到
- 然后回头继续第一个皇后放第二列,后面继续循环执行1,2,3,4的步骤。
排序
分类:
- 内部排序:所有数据都加载到内部存储器中进行排序。
- 插入排序
- 直接插入排序
- 希尔排序
- 选择排序
- 简单选择排序
- 堆排序
- 交换排序
- 冒泡排序
- 快速排序
- 归并排序
- 基数排序
- 插入排序
- 外部排序:由于数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。
时间复杂度
时间频度:
一个算法花费的时间与算法中语句的执行次数成正比,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度,记为T(n)。
- 一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
- T(n)不同,但时间复杂度可能相同。如T(n)=n2+7n+6与T(n)=3n2+2n+2它们的T(n)不同,但时间复杂度相同,都为O(n2)。
- 计算时间复杂度的方法:
- 用常数1代替运行时间中的所有加法常数
- 修改后的运行次数函数中,只保留最高阶项
- 去除最高阶项的系数
常见的时间复杂度:
-
常数阶O(1):无论代码执行了多少行,只要没有循环等复杂结构,那么时间复杂度就都是O(1)。
int i=1; int j=2; ++i; j++; int m=i+j;
代码执行的时候,消耗的时间不随某个变量的增长而增长。
-
对数阶O(log2n):
int i=1; while(i<n){ i=i*2; }
在while循环里,每次都将i乘以2,乘完之后,i距离n就越来越近了,假设循环x次之后,i就大于n了,此时循环退出,也就是说2的x次方等于n,那么x=log2n,也就是说当循环log2n次以后,这个代码就结束了,因此代码复杂度为O(log2n)。这个2是根据代码变化的,若i=i*3,则是O(log3n)。
-
线性阶O(n)
单层for循环就是线性阶
for(i=1;i<=n;i++){ j=i; j++; }
-
线性对数阶O(nlog2n)
将时间复杂度为O(logn)的代码循环N遍,其时间复杂度就是n*O(logn),即O(nlogn);
for(m=1;m<n;m++){ i=1; while(i<n){ i=i*2; } }
-
平方阶O(n2)
如果把O(n)的代码再嵌套循环一遍,他的时间复杂度就是O(n2)。如下图所示,若把其中一层for循环的次数改为m,则其时间复杂度为O(m*n)
for(x=1;i<=n;x++){ for(i=1;i<=n;i++){ j=i; j++; } }
-
立方阶O(n3)
类似5
-
k次方阶O(nk)
类似5
-
指数阶O(2n)
常见算法时间复杂度由小到大依次为:O(1)<O(log2n)<O(n)<O(nlog2n)<O(n2)<O(n3)<O(nk)<O(2n)<O(n!)
平均时间复杂度:所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。
最坏时间复杂度:最坏情况下的时间复杂度。一般讨论的复杂度均是最坏情况下的时间复杂度。
空间复杂度
定义:该算法所耗费的存储空间。是对一个算法在运行过程中临时占用存储空间大小的量度。
冒泡排序
通过对待排序的序列从前向后依次比较相邻元素的值,若发现逆序则交换。
- 一共进行数组大小 -1 次大的循环
- 每一趟排序的次数在逐渐的减少
- 如果我们发现在某趟排序中,没有发生一次交换,可以提前结束冒泡排序,这个就是优化。
public class BubbleSort{
public static void main(String[] args){
int[] arr = {3, 9, -1, 10, -2};
System.out.println("排序前");
System.out.println(Arrays.toString(arr));
bubbleSort(arr);
System.out.println("排序后");
System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int[] arr){
int temp = 0; //临时变量
//冒泡排序,时间复杂度O(n^2)
boolean flag = false; //标识变量,表示是否进行过交换
for (int i = 0; i < arr.length - 1; i++){
for(int j = 0; j < arr.length -1 -i; j++){
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; //重置flag,进行下次判断
}
}
}
}
选择排序
基本思想:寻找最小值过程中不进行交换
- 第一次从arr[0]~arr[n-1]中选取最小值,与arr[0]交换;
- 第二次从arr[1]~arr[n-1]中选取最小值,与arr[1]交换;
- 第 i 次从arr[i-1]~arr[n]中选取最小值,与arr[i-1]交换;
- 第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换;
- 总共通过n-1次,得到一个从小到大的有序序列。
说明:
- 选择排序一共进行数组大小 - 1次大排序循环
- 每一轮排序,又是一个小循环,循环的规则如下:
- 先假定当前这个数是最小数
- 然后和后面的每个数进行比较,如果发现有比当前数更小的数,就重新确定最小数,并得到下标
- 当遍历到数组的最后时,就得到本轮最小数和下标
- 交换
import java.text.SimpleDateFormat;
import java.util.Date;
public class SelectSort {
public static void main(String[] args){
int[] arr = new int[80000];
for (int i = 0; i < arr.length; i++){
arr[i] = (int)(Math.random() * 8000000); //生成一个[0,8000000)的数据
}
Date date1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(date1);
System.out.println("排序前的时间是="+date1Str);
selectSort(arr);
Date date2 = new Date();
String date2Str = simpleDateFormat.format(date2);
System.out.println("排序后的时间是="+date2Str);
}
public static void selectSort(int[] arr){
//选择排序时间复杂度是O(n^2)
for (int i = 0; i<arr.length-1; i++){
int minIndex = i;
int min = arr[i];
for (int j = i+1; j< arr.length;j++){
if (min>arr[j]){ //说明假定的最小值并不是最小
min=arr[j]; //重置min
minIndex=j; //重置minIndex
}
}
//将最小值放在arr[i],即交换
if(minIndex != i){
arr[minIndex]=arr[i];
arr[i]=min;
}
}
}
}
插入排序
基本思想:
把n个待排序的元素看成为一个有序表和一个无序表,开始时有序表中只包含一个元素,无序表中包含n-1个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较,将它插入到有序表中的适当位置,使之成为新的有序表。
import java.util.Arrays;
public class InsertSort {
public static void main(String[] args){
int[] arr={101,34,119,1,-1,89};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void insertSort(int[] arr){
int insertVal=0;
int insertIndex=0;
for (int i=1;i<arr.length;i++){
//定义待插入的数
insertVal=arr[i];
insertIndex=i-1; //即arr[i]前面的这个数下标
//给insertVal找到插入的位置
//说明
//1.insertIndex >=0保证在给insertVal找插入位置时不越界
//2。insertVal < arr[insertIndex]待插入的数,还没有找到插入位置
//3.就需要将arr[insertIndex]后移
while(insertIndex >=0 && insertVal<arr[insertIndex]){
arr[insertIndex+1]=arr[insertIndex]; //
insertIndex--;
}
//当退出while循环时说明插入位置找到,为insertIndex+1
arr[insertIndex+1]=insertVal;
}
}
}
希尔排序
基本思想:
将下标按一定增量进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个序列被分成一组,算法终止。
希尔排序---交换法
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args){
int[] arr={8,9,1,7,2,3,5,4,6,0};
shellSort(arr);
}
public static void shellSort(int[] arr){
int temp = 0;
int count = 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;
}
}
}
System.out.println("希尔排序第"+(++count)+ "轮="+ Arrays.toString(arr));
}
}
}
希尔排序---移动法
import java.util.Arrays;
public class ShellSort {
public static void main(String[] args){
int[] arr={8,9,1,7,2,3,5,4,6,0};
shellSort2(arr);
System.out.println(Arrays.toString(arr));
}
// public static void shellSort(int[] arr){
// int temp = 0;
// int count = 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;
// }
// }
// }
// System.out.println("希尔排序第"+(++count)+ "轮="+ Arrays.toString(arr));
// }
// }
public static void shellSort2(int[] arr){
//增量gap,并逐步缩小增量
for(int gap=arr.length/2;gap>0;gap/=2){
//从第gap个元素,逐个对其所在的组进行直接插入排序
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;
}
//当退出while后,就给temp找到插入的位置
arr[j]=temp;
}
}
}
}
}
快速排序
基本思想:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据编程有序序列。
import java.util.Arrays;
public class QuickSort {
public static void main(String[] args){
int[] arr=new int[] {3,4,6,7,2,7,2,8,0,9,1};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int start,int end){
if(start<end){
//把数组中的第0个数字作为标准数
int pivot=arr[start];
//记录需要排序的下标
int low=start;
int high=end;
//循环找比标准数大的数和比标准数小的数
while(low<high){
//右边的数字比标准数大
while(low<high && pivot<=arr[high]){
high--;
}
//使用右边的数字替换左边的数
arr[low]=arr[high];
//如果左边的数字比标准数小
while(low<high && arr[low]<=pivot){
low++;
}
arr[high]=arr[low];
}
//把标准数赋给低或者高所在位置(此时低==高)
arr[low]=pivot;
//处理所有小的数字
quickSort(arr, start, low-1);
//处理所有大的数字
quickSort(arr, low+1, end);
}
}
}
归并排序
归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案“修补”在一起,即分而治之)。
当分到每组只有一个元素时,这时每个部分就是有序的了,因此下面回溯每次合并两个有序数组,直到最后。
public class MergeSort {
public static void main(String[] args){
int[] arr={8,4,5,7,1,3,6,2};
mergeSort(arr,0,arr.length-1);
System.out.println(Arrays.toString(arr));
}
//分+合方法
public static void mergeSort(int[] arr, int left, int right){
if(left<right){
int mid=(left+right)/2; //中间索引
//向左递归进行分解
mergeSort(arr, left, mid);
//向右递归进行分解
mergeSort(arr, mid+1, right);
//合并
merge(arr, left, mid, right);
}
}
//合并的方法
/**
*
* @param arr 排序数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
*/
public static void merge(int[] arr, int left, int mid, int right){
//定义一个临时数组
int[] temp = new int[right-left+1];
int i=left; //初始化i,左边有序序列的初始索引
int j=mid+1; //初始化j,右边有序序列的初始索引
int t=0; //指向temp数组的当前索引
//(一)
//先把左右两边(有序)的数据按照规则填充到temp数组
//直到左右两边的有序序列,有一边处理完毕为止
while(i<=mid && j<=right){ //继续
//如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
//即将左边的当前元素,填充到temp数组
//然后 t++,i++
if(arr[i]<=arr[j]){
temp[t++]=arr[i++];
}
else{ //反之,将右边有序序列的当前元素,填充到temp数组
temp[t++]=arr[j++];
}
}
//(二)
//把有剩余数据的一边的数据依次全部填充到temp
while(i<=mid){ //左边的有序序列还有剩余的元素,就全部填充到temp
temp[t++]=arr[i++];
}
while(j<=right){ //右边的有序序列还有剩余的元素,就全部填充到temp
temp[t++]=arr[j++];
}
//(三)
//将新数组中的元素,覆盖nums旧数组中的元素。
//此时数组的元素已经是有序的
for(int k =0; k< temp.length;k++){
arr[left+k] = temp[k];
}
}
}
基数排序
基本思想:将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。
import java.util.Arrays;
public class RadixSort {
public static void main(String[] args){
int[] arr=new int[] {53,3,542,748,14,214};
radixSort(arr);
System.out.println(Arrays.toString(arr));
}
//基数排序
public static void radixSort(int[] arr){
//1.得到数组中最大的数的位数
int max=arr[0];
for (int i=1; i<arr.length; i++){
if(arr[i]>max){
max=arr[i];
}
}
//得到最大数是几位数
int maxLength=(max+"").length();
//定义一个二维数组,表示10个桶,每个桶就是一个一维数组
//说明
//1.二维数组包含10个一维数组
//2.为了防止在放入数的时候数据溢出,则每个一维数组(桶)大小定义为arr.length
//3.明确,基数排序就是使用空间换时间的经典算法
int[][] bucket=new int[10][arr.length];
//为了记录每个桶中实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入数据的个数
//可以这样理解
//比如:bucketElementCounts[0],记录的就是bucket[0]桶的放入数据个数
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){
//循环该桶,即第k个桶,放入
for(int l=0;l<bucketElementCounts[k];l++){
//取出元素放入到arr
arr[index++]=bucket[k][l];
}
}
//第l轮处理后,需要将每个bucketElementCounts[k]=0!!!
bucketElementCounts[k]=0;
}
}
}
}
说明:
- 基数排序是对传统桶排序的扩展
- 基数排序是经典的空间换时间的方式,占用内存很大,当海量数据排序时,容易造成OutOfMemoryError
- 基数排序是稳定的,经过排序后,数字的相对位置不变,即若原序列中r[i]=r[j],且r[i]在r[j]前面,则在排序后的序列中,r[i]仍然在r[j]之前,则称这种排序算法是稳定的,否则是不稳定的。
- 有负数的数组,我们不用基数排序。
堆排序
堆排序基本介绍:
-
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度均为O(nlogn),它也是不稳定排序。
-
堆是一个完全二叉树,每个节点的值都大于或等于其左右子节点的值,称为大顶堆(没有要求节点左右子节点的大小关系)。特点:arr[i]>=arr[2*i+1],arr[i]>=arr[2*i+2]。
-
每个节点的值都小于或等于其左右子节点的值,称为小顶堆。特点:arr[i]<=arr[2*i+1],arr[i]<=arr[2*i+2]。
-
一般,升序采用大顶堆,降序采用小顶堆。
基本思想:
- 将待排序的序列按照从右到左,从下到上的规则构造成一个大顶堆
- 此时,整个序列的最大值就是堆顶的根节点
- 将其与末尾元素进行交换,此时末尾就为最大值
- 然后将剩余 n-1 个元素重新构造成一个堆,这样会得到n个元素的次小值,如此反复,便能得到一个有序序列了。
package sort;
import java.util.Arrays;
public class HeapSort {
public static void main(String[] args) {
//要求将数组进行升序排序
int[] arr={4,6,8,5,9,-1,3};
heapSort(arr);
}
//编写一个堆排序的方法
public static void heapSort(int[] arr) {
int temp=0;
System.out.println("堆排序!!");
for (int i=arr.length/2-1;i>=0;i--){
adjustHeap(arr,i,arr.length);
}
//将堆顶元素与末尾元素交换,将最大元素“沉”到数组的末端
//重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序
for (int j=arr.length-1;j>0;j--) {
//交换
temp=arr[j];
arr[j]=arr[0];
arr[0]=temp;
//由于只交换根节点和最后一个节点,因此刨除此时新的root节点,以第二层节点为父节点的树已经是大顶堆
//故,只需要从i=0开始进行大顶堆构造即可
adjustHeap(arr,0,j);
}
System.out.println("数组=" + Arrays.toString(arr));
}
//将一个数组(二叉树),调整成一个大顶堆
/**
* 完成将以i为父节点的树转换成大顶堆
* 举例:arr={4,6,8,5,9}; => i=1 => adjustHeap => 得到{4,9,8,5,6}
* 如果我们再次调用adjustHeap,传入的是i=0 => 得到{9,6,8,5,4}
* @param arr 待调整的数组
* @param i 表示非叶子节点在数组中索引
* @param length 表示对多少个元素继续调整,length在逐渐减少
*/
public static void adjustHeap(int[] arr,int i,int length) {
int temp=arr[i]; //先取出当前元素的值,保存在临时变量
//开始调整
//说明
//1.k是i节点的左子节点
for (int k=2*i+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; //!!! i指向k,继续循环比较 因为arr[k]位置元素变动,下次要重新对以k为父节点的树进行排序
}
else {
break; //!!!
}
}
//当for循环结束后,我们已经将以i为父节点的树的最大值放在了父节点位置
arr[i]=temp; //将temp的值放在调整后的位置
}
}
排序算法的时间复杂度
说明:
- 稳定:相同数字的相对位置不变;
- 内排序:所有排序都在内存中完成;
- 外排序:由于数据量大,因此把数据放在磁盘中,而排序通过磁盘和内存的数据传输才能进行;
- 时间复杂度:一个算法执行所耗费的时间;
- 空间复杂度:运行完一个程序所需内存的大小;
- n:数据规模;
- k:“桶”的个数/最大数字的位数
- in-place:不占用额外的内存
- out-place:占用额外的内存
查找
顺序(线性)查找
public class SeqSearch {
public static void main(String[] args){
int[] arr={1,9,11,-1,34,89}; //没有顺序的数组
int index=seqSearch(arr,11);
if(index==-1){
System.out.println("没有找到");
}
else{
System.out.println("找到,下标=" + index);
}
}
/**
* 这里我们实现的线性查找就是找到一个满足条件的值就返回
* @param arr
* @param value
*/
public static int seqSearch(int[] arr, int value){
//线性查找就是逐一比对,发现有相同值就返回下标
for (int i=0;i<arr.length;i++){
if(arr[i]==value){
return i;
}
}
return -1;
}
}
二分查找/折半查找
针对一个有序数组,进行二分查找。
思路分析:
- 首先确定该数组的中间下标 mid=(left+right)/2
- 然后让需要查找的数 findVal 和 arr[mid] 进行比较
- findVal > arr[mid],说明你要查找的数在mid的右边,因此需要递归向右查找
- findVal < arr[mid],说明你要查找的数在mid的左边,因此需要递归向左查找
- findVal = arr[mid],说明找到,就返回
- 结束递归条件
- 找到就结束递归
- 递归完整个数组,仍然没有找到findVal,也需要结束递归,当 left>right就需要退出。
import java.sql.Array;
import java.util.ArrayList;
import java.util.List;
public class BinarySearch {
public static void main(String[] args){
int[] arr={1,8,10,89,1000,1000,1000,1234};
// int resIndex=binarySearch(arr,0,arr.length-1,88);
//
// System.out.println("下标为:"+resIndex);
List<Integer> resIndexList=binarySearch2(arr,0,arr.length-1,1000);
System.out.println(resIndexList);
}
//二分查找法
/**
*
* @param arr 数组
* @param left 左边索引
* @param right 右边索引
* @param findVal 要查找的值
* @return 如果找到就返回下标,如果没有找到,就返回-1
*/
public static int 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<midVal){ //向左递归
return binarySearch(arr, left, mid-1, findVal);
}
else{ //正好为中间值
return mid;
}
}
//改进的二分查找
public static List<Integer> binarySearch2(int[] arr, int left, int right, int findVal){
//当left>right时,说明递归整个数组,仍然没有找到该值
if(left>right){
return new ArrayList<Integer>();
}
int mid=(left+right)/2;
int midVal = arr[mid];
if(findVal>midVal){ //向右递归
return binarySearch2(arr, mid+1, right, findVal);
}
else if(findVal<midVal){ //向左递归
return binarySearch2(arr, left, mid-1, findVal);
}
else{ //正好为中间值
//思路分析
//1.再找到mid索引值,不要马上返回
//2.向mid索引值的左边扫描,将所有满足1000的元素下标,加入到集合ArrayList
//3.向mid索引值的右边扫描,将所有满足1000的元素下标,加入到集合ArrayList
//4.将ArrayList返回
List<Integer> resIndexList=new ArrayList<Integer>();
//向mid索引值的左边扫描,将所有满足1000的元素下标,加入到ArrayList
int temp=mid-1;
while(true){
if (temp<0 || arr[temp] != findVal){
break;
}
//否则,就temp放入到resIndexList
resIndexList.add(temp);
temp-=1;
}
resIndexList.add(mid);
//向mid索引值的右边扫描,将所有满足1000的元素下标,加入到ArrayList
temp=mid+1;
while(true){
if (temp>arr.length-1 || arr[temp] != findVal){
break;
}
//否则,就temp放入到resIndexList
resIndexList.add(temp);
temp+=1;
}
return resIndexList;
}
}
}
插值查找
基本原理:
-
插值查找类似与二分查找,不同的是插值查找每次从自适应mid处开始查找。
-
$$
mid=left+\frac{findVal-arr[left]}{arr[right]-arr[left]} (right-left)
$$
public class InsertValueSearch {
public static void main(String[] args){
int[] arr=new int[100];
for (int i=0;i<100;i++){
arr[i]=i+1;
}
int index=insertValueSearch(arr,0,arr.length-1,1);
System.out.println("数组下标为:"+index);
}
//编写插值查找法
//说明:插值查找算法也需要有序
/**
*
* @param arr 数组
* @param left 左边索引
* @param right 右边索引
* @param findVal 要查找的值
* @return 如果找到就返回对应下表,如果没找到就返回-1
*/
public static int insertValueSearch(int[] arr,int left,int right,int findVal){
//findVal<arr[0]和findVal>arr[arr.length-1])是必须的,防止findVal过大或过小,mid导致数组越界
if (left>right || findVal<arr[0] || findVal>arr[arr.length-1]){
return -1;
}
//求出mid
int mid=left + (right - left)*(findVal-arr[left])/(arr[right]-arr[left]);
int midVal=arr[mid];
if(findVal>midVal){ //向右递归
return insertValueSearch(arr,mid+1,right,findVal);
}
else if (findVal<midVal){ //向左递归
return insertValueSearch(arr,left,mid-1,findVal);
}
else{
return mid;
}
}
}
说明:
- 对于数据量较大,数字分布比较均匀的序列来说,采用插值查找速度较快。
- 序列分布不均匀时,效率不一定比二分查找好。
斐波那契(黄金分割法)查找
斐波那契(黄金分割法)原理:与前两种相似,仅仅改变了中间节点(mid)的位置,mid 不再是中间或插值得到,而是位于黄金分割点附近,即 mid=low+F[k-1]-1(F代表斐波那契数列)。
对F[k-1]-1的理解:
-
由斐波那契数列 F[k]=F[k-1]-F[k-2]的性质,可以得到 (F[k]-1)=(F[k-1]-1) + (F[k-2]-1) +1。该式说明,只要顺序表的长度为F[k]-1,则可以将该表分成长度为 F[k-1]-1 和 F[k-2]-1 的两段,即mid=low+F[k-1]-1。
-
类似的,每一子段也可以用相同的方式分割。
-
但顺序表的长度n不一定刚好等于F[k]-1,所以需要将原来的顺序表长度n增加至F[k]-1。这里的k值只要能使F[k]-1恰好大于或等于n即可,由一下代码得到,顺序表长度增加后,新增的位置(从n+1到F[k]-1位置),都赋为n位置的值即可。
while(n>fib(k)-1){ k++; }
import java.util.Arrays;
public class FibonacciSearch {
public static int maxsize=20;
public static void main(String[] args){
int[] arr={1,8,10,89,1000,1234};
System.out.println("index="+fibSearch(arr,1000));
}
//因为后面我们mid=low+F[k-1]-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;
}
//编写斐波那契查找算法
//使用非递归的方式编写算法
/**
*
* @param arr 数组
* @param key 需要查找的关键码(值)
* @return 返回对应下标,没有返回-1
*/
public static int fibSearch(int[] arr,int key){
int low = 0;
int high=arr.length-1;
int k=0; //表示斐波那契分割数值的下标
int mid=0;
int[] f=fib(); //获取到斐波那契数列
//获取到斐波那契分割数值的下标
while (high>f[k]-1){
k++;
}
//因为 F[k]值可能大于arr的长度,因此我们需要使用Arrays类,构造一个新的数组,并指向temp[]
//不足的部分会使用0填充
int[] temp= Arrays.copyOf(arr,f[k]);
//实际上需要使用arr数组最后的数填充temp
for (int i=high+1;i<temp.length;i++){
temp[i]=arr[high];
}
//使用while来循环处理,找到我们的数key
while(low<=high){ //只要这个条件满足就可以一直找
mid=low+f[k-1]-1;
if(key<temp[mid]){ //我们应该继续向数组左边查找
high=mid-1;
//为什么是 k--
//说明
//1.全部元素=前面的元素+后面的元素
//2.F[k]=F[k-1]+F[k-2]
//因为前面有F[k-1]个元素,所以可以继续拆分F[k-1]=F[k-2]+F[k-3]
//即在F[k-1]的前面继续查找 k--
//即下次循环mid=low+F[k-1-1]-1
k--;
}
else if(key>temp[mid]){ //向数组右边查找
low=mid+1;
//为什么是 k-=2
//说明
//1.全部元素=前面的元素+后面的元素
//2.F[k]=F[k-1]+F[k-2]
//3.因为后面有F[k-2]个元素,所以可以继续拆分F[k-2]=F[k-3]+F[k-4]
//即在F[k-2]的前面继续查找 k-=2
//即下次循环mid=low+F[k-2-1]-1
k-=2;
}
else{ //找到了
//需要确定,返回的是哪个下标
if(mid<=high){
return mid;
}
else{
return high;
}
}
}
return -1;
}
}
程序员常用的10大算法
二分查找算法(非递归)
- 二分查找法只适用于从有序的数列中进行查找(比如数字和字母等),将数列排序后再进行查找。
- 二分查找法的运行时间为对数时间O(log2n),即查找到需要的目标位置最多只需要log2n
步。
package algorithm.binarysearch;
public class BinarySearchNoRecur {
public static void main(String[] args) {
int[] arr = {1, 3, 8, 10, 11, 67, 100};
int index = binarySearch(arr, 1);
System.out.println("index="+index);
}
//二分查找的非递归实现
/**
*
* @param arr 待查找的数组
* @param target 需要查找的数
* @return 返回对应下标,-1表示没有找到
*/
public static int binarySearch(int[] arr, int target) {
int left = 0;
int right = arr.length-1;
while (left <= right) {//说明继续查找
int mid = (left + right) / 2;
if (arr[mid] == target) {
return mid;
}
else if (arr[mid] > target) {
right = mid - 1; //需要向左查找
}
else {
left = mid + 1; //需要向右查找
}
}
return -1; //找不到
}
}
分治算法
分治算法在每一层递归上都有三个步骤:
- 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题
- 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
- 合并:将各个子问题的解合并为原问题的解
分治算法解决汉诺塔思路:
- 如果有n=1个盘,A->C
- 如果有n>=2个盘,我们总可以看做是两个盘,即最下面的一个盘 和 上面的所有盘
2.1 先把上面的所有盘 A->B
2.2 把最下面的一个盘 A->C
2.3 把B塔的所有盘从 B->C
package algorithm.dac;
public class Hanoitower {
public static void main(String[] args) {
hanoiTower(5,'A','B','C');
}
//汉诺塔移动方法
//使用分治算法解决
public static void hanoiTower(int num, char a, char b, char c) {
//如果只有一个盘
if (num == 1) {
System.out.println("第一个盘从 "+a+"->"+c);
}
else {
//如果我们有n>=2个盘,我们总可以看做是两个盘,即最下面的一个盘 和 上面的所有盘
//1.先把上面的所有盘 A->B
hanoiTower(num - 1, a, c, b);
//2.把最下面的一个盘 A->C
System.out.println("第"+num+"个盘从 "+a+"->"+c);
//3.把B塔的所有盘从 B->C
hanoiTower(num-1, b, a, c);
}
}
}
动态规划算法
- 动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。
- 动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
- 与分治法不同的是,适合于用动态规划求解的问题,经分解得到的子问题往往不是互相独立的。(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步求解)。
- 动态规划可以通过填表的方式来逐步推进,得到最优解。
背包问题思路:
- 利用动态规划。每次遍历到的第i个物品,根据w[i]和v[i]来确定是否需要将该物品放入背包。即对于给定的n个物品,设v[i]、w[i]分别为第i个物品的价值和重量,C为背包的容量。再令v[i][j]表示在前i个物品中能够装入容量j的背包最大价值,则有
- v[i][0]=v[0][j]=0;
-
表示填入表的第一行和第一列都为0
-
- 当w[i]>j时:v[i][j]=v[i-1][j]
-
当准备加入新增的商品的容量大于当前背包的容量时,就直接使用上一个单元格的装入策略
-
- 当j>=w[i]时:v[i][j]=max
-
当准备加入的新增的商品的容量小于等于当前背包的容量,装入方式:
-
v\[i-1][j]:上一个单元格装入值
-
v[i]:当前商品价值
-
v\[i-1][j-w[i]]:装入i-1商品 到容量为j-w[i]的价值
-
package algorithm.dynamic;
public class KnapsackProblem {
public static void main(String[] args) {
int[] w = {1, 4, 3}; //物品重量
int[] val = {1500, 3000, 2000}; //物品价值
int m = 4; //背包容量
int n = val.length; //物品个数
//为了记录放入商品的情况,定义一个二维数组
int[][] path = new int[n+1][m+1];
//创建二维数组
//v[i][j] 表示i个物品装入容量为j的背包最大价值
int[][] v = new int[n+1][m+1];
//初始化第一行,第一列
for (int i=0; i<v.length; i++) {
v[i][0]=0; //第一列为0
}
for (int i = 0; i < v[0].length; i++) {
v[0][i]=0; //第一行为0
}
//动态规划处理
for (int i = 1; i < v.length; i++) { //不处理第一行
for (int j = 1; j < v[0].length; j++) { //不处理第一列
//套用公式
if (w[i-1]>j) { //因为程序i从1开始,原公式i修改i-1
v[i][j]=v[i-1][j];
}
else {
//说明:因为i从1开始,公式需要调整
//v[i][j]=Math.max(v[i-1][j],val[i-1]+v[i-1][j-w[i-1]]);
//为了记录商品存放背包的情况,不能简单的使用Math.max,需要使用if-else
if (v[i-1][j]<val[i-1]+v[i-1][j-w[i-1]]) {
v[i][j] = val[i-1]+v[i-1][j-w[i-1]];
//把当前情况记录
path[i][j]=1;
}
else {
v[i][j] = v[i-1][j];
}
}
}
}
//输出结果
for (int i = 0; i < v.length; i++) {
for (int j = 0; j < v[i].length; j++) {
System.out.print(v[i][j]+" ");
}
System.out.println();
}
//输出放入的哪些商品
int i = path.length-1; //行的最大下标
int j = path[0].length-1; //列的最大下标
while (i>0 && j>0) {
if (path[i][j] == 1) {
System.out.printf("第%d个商品放入背包\n",i);
j-=w[i-1];
}
i--;
}
}
}
KMP算法
很详尽KMP算法(厉害) - ZzUuOo666 - 博客园 (cnblogs.com)
例:给定文本串S“BBC ABCDAB ABCDABCDABDE”,和模式串P“ABCDABD”。
让文本串和模式串一一匹配,即S[0] ?= P[0]
,S[0] != P[0],则i++,j=0,即S[1] ?= P[0]
,若不匹配,则继续,直到有匹配的,即
,此时,i++,j++,即匹配S[4] ?= P[1],即
,重复i++,j++,直到不匹配,即
,此时匹配字符为ABCDAB,查询最大长度表,有ABCDAB===>2(匹配表为前缀和后缀最大公共元素长度)
,则模式串向右移动6-2=4,即
此时,C并不匹配,模式串继续右移2-0=2,即
,失配,右移1位,即
,此时C与D失配,继续右移6-2=4,即
匹配成功。
移位原理:
如上图(a)所示,此时 i 的位置对应元素与 j 位置对应元素失配,同时模式串 0~(j-1) 位元素与文本串 i 前对应位元素相匹配,根据模式串 0~(j-1) 位子串对应的部分匹配表知,其前缀字串与文本串 i 前位子串的后缀相同,因此,保持 i 不动,j 移动到 next[j-1] ,如图(b)。
package algorithm.kmp;
import java.util.Arrays;
public class KMPAlgorithm {
public static void main(String[] args) {
String str1 = "BBC ABCDAB ABCDABCDABDE";
String str2 = "ABCDABD";
int[] next = kmpNext("ABCDABD");
System.out.println("next=" + Arrays.toString(next));
int index = kmpSearch(str1, str2, next);
System.out.println("index=" + index);
}
//写出我们kmp搜索算法
/**
*
* @param str1 文本串
* @param str2 模式串
* @param next 部分匹配表,是子串对应的部分匹配表
* @return 如果是-1就是没有匹配到,否则返回第一个匹配的位置
*/
public static int kmpSearch(String str1, String str2, int[] next) {
//遍历
for (int i = 0, j = 0; i < str1.length(); i++) {
//需要处理str1.charAt(i) != str2.charAt(j),调整j的大小
//kmp算法核心点
while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
j = next[j-1];
}
if (str1.charAt(i) == str2.charAt(j)) {
j++;
}
if (j == str2.length()) { //找到了
return i-j+1;
}
}
return -1;
}
//获取一个字符串(子串)的部分匹配值
public static int[] kmpNext(String dest) {
//创建一个next数组保存部分匹配值
int[] next = new int[dest.length()];
next[0] = 0; //如果字符串长度为1,部分匹配值为0
for (int i = 1,j = 0; i < dest.length(); i++) {
//当dest.charAt(i) != dest.charAt(j),我们需要从next[j-1]获取新的j
//直到我们发现有 dest.charAt(i)==dest.charAt(j) 成立才退出
//这是kmp算法的核心点
while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
j = next[j-1];
}
//当dest.charAt(i) == dest.charAt(j) 满足时,部分匹配值就+1
if (dest.charAt(i) == dest.charAt(j)) {
j++;
}
next[i]=j;
}
return next;
}
}
贪心算法
贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果最好或者最优的算法。
贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对接近最优解。
案例:集合覆盖问题
思路分析:
- 遍历所有广播电台,找到一个覆盖了最多未覆盖的地区的电台
- 将这个电台加入到一个集合中(例如ArrayList),并把该电台覆盖的地区在下次比较时去掉
- 重复第1步,直到覆盖了全部地区。
package algorithm.greedy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
public class GreedyAlgorithm {
public static void main(String[] args) {
//创建广播电台,放入到Map
HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
//将各个电台放入到broadcasts
HashSet<String> hashSet1 = new HashSet<>();
hashSet1.add("北京");
hashSet1.add("上海");
hashSet1.add("天津");
HashSet<String> hashSet2 = new HashSet<>();
hashSet2.add("广州");
hashSet2.add("北京");
hashSet2.add("深圳");
HashSet<String> hashSet3 = new HashSet<>();
hashSet3.add("成都");
hashSet3.add("上海");
hashSet3.add("杭州");
HashSet<String> hashSet4 = new HashSet<>();
hashSet4.add("上海");
hashSet4.add("天津");
HashSet<String> hashSet5 = new HashSet<>();
hashSet5.add("杭州");
hashSet5.add("大连");
//加入到Map
broadcasts.put("K1",hashSet1);
broadcasts.put("K2",hashSet2);
broadcasts.put("K3",hashSet3);
broadcasts.put("K4",hashSet4);
broadcasts.put("K5",hashSet5);
//allAreas 存放所有地区
HashSet<String> allAreas = new HashSet<>();
//迭代生成
//for (HashSet<String> value: broadcasts.values()) {
// allAreas.addAll(value);
//}
allAreas.add("北京");
allAreas.add("上海");
allAreas.add("天津");
allAreas.add("广州");
allAreas.add("深圳");
allAreas.add("成都");
allAreas.add("杭州");
allAreas.add("大连");
//创建ArrayList,存放选择的电台
ArrayList<String> selects = new ArrayList<>();
//定义一个临时集合,在遍历过程中,存放电台覆盖集合与当前剩余地区的交集
HashSet<String> tempSet = new HashSet<String>();
//定义一个maxKey,保存在一次遍历过程中,能够覆盖最多地区对应的电台的key
//如果maxKey不为空,则会加入到selects
String maxKey = null;
//定义一个集合,表示最大电台覆盖区域
HashSet<String> maxKeyAreas = new HashSet<String>();
while (allAreas.size() != 0) { //如果allAreas不为0,则表示还没有覆盖到所有地区
//每进行一个while,需要maxKey置空
maxKey = null;
//遍历 broadcasts,取出对应的key
for (String key : broadcasts.keySet()) {
//没进行一次for
tempSet.clear();
//当前这个key能够覆盖的地区
HashSet<String> areas = broadcasts.get(key);
tempSet.addAll(areas);
//求出tempSet和allAreas集合的交集,交集会赋给tempSet
tempSet.retainAll(allAreas);
//定义一个集合,表示最大电台覆盖区域
if (maxKey != null) {
maxKeyAreas = broadcasts.get(maxKey);
maxKeyAreas.retainAll(allAreas);
}
//如果当前电台覆盖的区域 多于 maxKey对应电台覆盖的区域 则maxKey=key;
if (tempSet.size() > 0 &&
(maxKey == null || tempSet.size() > maxKeyAreas.size())) {
maxKey=key;
}
}
//maxKey != null,就应该加入selects集合
if (maxKey != null) {
selects.add(maxKey);
//将maxKey指向的电台覆盖区域从allAreas集合中删除
allAreas.removeAll(broadcasts.get(maxKey));
}
}
System.out.println("得到的选择结果为:"+selects);
}
}
普里姆算法(Prim)
最小生成树:Minimum Cost Spanning Tree,简称MST
- 给定一个无向连通图,保持图的连通性,且边上权的总和最小,就称为最小生成树
- N个顶点,一定有N-1条边
- 包含全部顶点
- 最小生成树算法主要是 普里姆算法 和 克鲁斯卡尔算法
package algorithm.prim;
import java.util.Arrays;
public class PrimAlgorithm {
public static void main(String[] args) {
//测试图是否创建成功
char[] data = new char[] {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
int verxs = data.length;
//邻接矩阵关系使用二维数组描述
int[][] weight = new int[][] {
{10000,5,7,10000,10000,10000,2},
{5,10000,10000,9,10000,10000,3},
{7,10000,10000,10000,8,10000,10000},
{10000,9,10000,10000,10000,4,10000},
{10000,10000,8,10000,10000,5,4},
{10000,10000,10000,4,5,10000,6},
{2,3,10000,10000,4,6,10000}
};
//创建MGraph对象
MGraph graph = new MGraph(verxs);
//创建一个MinTree对象
MinTree minTree = new MinTree();
minTree.createGraph(graph,verxs,data,weight);
//输出
minTree.showGraph(graph);
//测试普里姆算法
minTree.prim(graph,0);
}
}
//创建最小生成树 ->村庄
class MinTree {
//创建图的邻接矩阵
public void createGraph(MGraph graph, int verxs, char[] data, int[][] weight) {
for (int i = 0; i < verxs; i++) {
graph.data[i] = data[i];
for (int j = 0; j < verxs; j++) {
graph.weight[i][j] = weight[i][j];
}
}
}
//显示图的邻接矩阵
public void showGraph(MGraph graph) {
for (int[] link : graph.weight) {
System.out.println(Arrays.toString(link));
}
}
//编写prim算法,得到最小生成树
/**
*
* @param graph 图
* @param v 表示从图的第几个顶点开始生成'A'->0 'B'->1......
*/
public void prim(MGraph graph, int v) {
//visited[] 标记结点(顶点)是否被访问过
int[] visited = new int[graph.verxs];
//visited[] 默认元素都是0,表示没有访问过
//把当前这个结点标记为已访问
visited[v] = 1;
//用h1 和 h2记录两个顶点的下标
int h1 = -1;
int h2 = -1;
//将minWeight初始化成一个大的数,后面在被遍历过程中会被替换
int minWeight = 10000;
//因为有 graph.verxs个顶点,普里姆算法结束后,有graph.verxs-1个边
for (int k = 1; k < graph.verxs; k++) {
//这个是确定每一次生成的子图,和哪两个结点的距离最近
for (int i = 0; i < graph.verxs; i++) { //遍历已经访问过的结点
for (int j = 0; j < graph.verxs; j++) { //遍历还未访问的结点
if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight) {
//替换minWeight
minWeight = graph.weight[i][j];
h1 = i;
h2 = j;
}
}
}
//此时找到一条边是最小
System.out.println("边<" + graph.data[h1] + "," + graph.data[h2] + "> 权值:" + minWeight);
//将当前这个结点标记为已经访问
visited[h2] = 1;
//minWeight重新设置为最大值10000
minWeight = 10000;
}
}
}
class MGraph {
int verxs; //表示图的结点个数
char[] data; //存放结点数据
int[][] weight; //存放边,就是我们的邻接矩阵
public MGraph(int verxs) {
this.verxs = verxs;
data = new char[verxs];
weight = new int[verxs][verxs];
}
}
克鲁斯卡尔算法(Kruskal)
基本思想:按照权值从小到大的顺序选择n-1条边,并保证这n-1条边不构成回路
具体做法:首先构造一个只含n个顶点的森林,然后依权值从小到大从连通网中选择边加入到森林中,并使森林中不产生回路,直至森林变成一棵树为止。
package algorithm.kruskal;
import java.util.Arrays;
public class KruskalCase {
private int edgeNum; // 边的个数
private char[] vertexs; // 顶点数组
private int[][] matrix; //邻接矩阵
//使用 INF 表示两个顶点不能连通
private static final int INF = Integer.MAX_VALUE;
public static void main(String[] args) {
char[] vertexs = {'A','B','C','D','E','F','G'};
int[][] matrix = {
{0, 12, INF, INF, INF, 16, 24},
{12, 0, 10, INF, INF, 7, INF},
{INF, 10, 0, 3, 5, 6, INF},
{INF, INF, 3, 0, 4, INF, INF},
{INF, INF, 5, 4, 0, 2, 8},
{16, 7, 6, INF, 2, 0, 9},
{14, INF, INF, INF, 8, 9, 0}
};
//创建KruskalCase 对象实例
KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
//输出
kruskalCase.print();
EData[] edges = kruskalCase.getEdges();
kruskalCase.sortEdges(edges);
System.out.println(Arrays.toString(edges));
kruskalCase.kruscal();
}
//构造器
public KruskalCase(char[] vertexs, int[][] matrix) {
//初始化顶点数和边的个数
int vlen = vertexs.length;
//初始化顶点,复制拷贝的方式
this.vertexs = new char[vlen];
for (int i = 0; i < vlen; i++) {
this.vertexs[i] = vertexs[i];
}
//初始化边
this.matrix = new int[vlen][vlen];
for (int i = 0; i < vlen; i++) {
for (int j = 0; j < vlen; j++) {
this.matrix[i][j] = matrix[i][j];
}
}
//统计边的数目
for (int i = 0; i < vlen; i++) {
for (int j = i+1; j < vlen; j++) {
if (this.matrix[i][j] != INF) {
this.edgeNum++;
}
}
}
}
public void kruscal() {
int index = 0; //表示最后结果数组的索引
int[] ends = new int[edgeNum]; //用来保存已有最小生成树中每个顶点在最小生成树中的终点
//创建结果数组,保存最后的最小生成树
EData[] rets = new EData[edgeNum];
//获取图中所有边的集合,一共有12条边
EData[] edges = getEdges();
//按照边的权值大小进行从小到大排序
sortEdges(edges);
//遍历edges,将边添加到最小生成树中,判断准备加入的边是否形成了回路,如果没有就加入到rets,否则不能加入
for (int i = 0; i < edgeNum; i++) {
//获取到第 i 条边的第一个顶点
int p1 = getPosition(edges[i].start);
//获取到第 i 条边的另一个顶点
int p2 = getPosition(edges[i].end);
//获取p1这个顶点在已有最小生成树中的终点
int m = getEnd(ends, p1);
//获取p2这个顶点在已有最小生成树中的终点
int n = getEnd(ends, p2);
//是否构成回路
if (m != n) { //不构成
ends[m] = n; //设置m在已有最小生成树中的终点
rets[index++] = edges[i]; //有一条边加入到rets数组
}
}
//统计并打印最小生成树,输出rets
System.out.println("最小生成树为=");
for (int i = 0; i < index; i++) {
System.out.println(rets[i]);
}
}
//打印邻接矩阵
public void print() {
System.out.println("邻接矩阵为:");
for (int i = 0; i < vertexs.length; i++) {
for (int j = 0; j < vertexs.length; j++) {
System.out.printf("%-12d", matrix[i][j]);
}
System.out.println();
}
}
// 对边进行排序处理,冒泡排序
private void sortEdges(EData[] edges) {
for (int i = 0; i < edges.length - 1; i++) {
for (int j = 0; j < edges.length - 1 - i; j++) {
if (edges[j].weight > edges[j+1].weight) { //交换
EData tmp = edges[j];
edges[j] = edges[j+1];
edges[j+1] = tmp;
}
}
}
}
/**
*
* @param ch 顶点的值,比如‘A’ ‘B’
* @return 返回ch顶点对应的下标,如果找不到,返回-1
*/
private int getPosition(char ch) {
for (int i = 0; i < vertexs.length; i++) {
if (vertexs[i] == ch) {
return i;
}
}
//找不到
return -1;
}
/**
* 功能:获取图中的边,放到 EData[] 数组中,后面我们需要遍历该数组
* 通过 matrix 邻接矩阵来获取
* EData[] 形式 [['A' 'B' 12], ['B' 'F' 7], ...]
* @return
*/
private EData[] getEdges() {
int index = 0;
EData[] edges = new EData[edgeNum];
for (int i = 0; i < vertexs.length; i++) {
for (int j = i + 1; j < vertexs.length; j++) {
if (matrix[i][j] != INF) {
edges[index++] = new EData(vertexs[i], vertexs[j], matrix[i][j]);
}
}
}
return edges;
}
/**
* 功能:获取下标为 i 的顶点的终点,用于后面判断两个顶点的终点是否相同
* @param ends 记录各个顶点对应的终点是哪个,ends 数组是在遍历过程中逐步形成的
* @param i 传入的顶点对应下标
* @return 下标为 i 这个顶点对应终点的下标
*/
private int getEnd(int[] ends, int i) {
while (ends[i] != 0) {
i = ends[i];
}
return i;
}
}
// 创建一个类EData,它的对象实例就表示一条边
class EData {
char start; // 边的一点
char end; // 边的另一点
int weight; //边的权值
// 构造器
public EData(char start, char end, int weight) {
this.start = start;
this.end = end;
this.weight = weight;
}
//重写toString方法,便于输出边的信息
@Override
public String toString() {
return "EData{" + "<" + start + ", " + end + ">=" + weight + '}';
}
}
迪杰斯特拉(Dijkstra)
迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。它主要特点是以起始点为中心向外层层层扩展(广度优先搜索思想),直到扩展到终点为止。