1、概述
数据结构概述
什么是数据结构
数据结构是指互相之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成
数据的存储结构
-
顺序存储结构
-
链式存储结构
两种方式的区别:
数据的逻辑结构
-
集合结构
集合结构中的数据元素属于一个集合,他们之间是并列的关系,除此之外没有其他关系
-
线性结构
线性结构中的元素存在着一对一的关系
-
树形结构
树形结构中的元素存在着一对多的关系
-
图形结构
图形结构中的元素存在着多对多的关系
算法概述
算法的定义
是指解题方案的准确而完整的描述,是一系列解决问题的清晰质量,算法代表着系统的方法描述解决问题的策略机制
算法的特性
- 输入
- 输出
- 有穷性
- 确定性
- 可行性
算法的基本要求
- 正确性
- 可读性
- 健壮性
- 时间复杂度
- 空间复杂度
2、线性结构
数组
数组的基本使用
-
创建数组:
int[] arr1 = new int[5];//创建一个长度为5的int类型的数组
-
访问数组中的元素
arr1[0];//取得数组arr1的第一个元素
-
为数组赋值
arr1[0] = 3;//设置数组arr1的第一个元素值为3(数组从0开始)
int[] arr2 = new int[]{1,3,4,5,6};//创建数组时为其赋值
-
数组元素的添加(扩容)
//在数组最后插入一个元素(本质就是新建一个数组,长度为原来数组的长度+1,把原数组拷贝过去,再把要插入的值插入到最后一个元素) int[] arr1 = new int[]{1,2,3,4,5,6}; int[] newArr = new int[arr1.length+1]; dst = 9; for(int i=0;i<arr.length;i++){ newArr[i]=arr[i] } newArr[arr.length]=dst;
在数组任意位置插入元素 int[] arr1 = new int[]{1,2,3,4,5,6}; int[] newArr = new int[arr1.length+1]; int dst = 999; int index = 2; for(int i=0;i<arr1.length;i++){ if(i<index){ newArr[i] = arr1[i]; }else{ newArr[i+1] = arr1[i]; } } newArr[index] = dst; arr1 = newArr; System.out.println(Arrays.toString(newArr));
-
数组元素的删除
//和添加原理相同(创建一个新数组,长度为原来数组长度-1,把原数组拷贝过去,进行判断,如果是想删除的元素就跳过这个元素,直接复制下一元素) int[] arr1 = new int[]{1,2,3,4,5,6}; int[] newArr = new int[arr1.length-1]; for(int i=0;i<newArr.length;i++){ if(i<2){ newArr[i] = arr1[i]; }else{ newArr[i] = arr1[i+1]; } } arr1=newArr;
面向对象的数组
对原来对数组的一系列操作进行封装,来分别实现创建、添加、查询、删除等操作
查找算法
线性查找
对每个元素进行遍历,找到就返回该元素的下标
//创建数组
int[] arr = new int[]{1,7,6,4,2,89,3,7};
//目标元素
int target = 4;
//目标所在的下标,如果没找到就返回-1
int index = -1;
//遍历数组
for(int i=0;i<arr.length;i++){
if(arr[i]==target){
index = i;
break;
}
}
System.out.println("该元素的下表为:"+index);
二分法查找
@Test
public void test07(){
//创建数组
int[] arr = new int[]{1,2,3,4,5,6,7,8,9};
//目标元素
int target = 8;
//开始位置
int begin = 0;
//结束位置
int end = arr.length-1;
//中间位置
int mid = (begin+end)/2;
//目标位置
int index = -1;
while(true){
//先判断目标元素是否在该数组范围内,因为是顺序排列,所以只要target大于最后一个元素或是小于第一个元素就可以断定target不在数组中
if(target>arr[arr.length-1]||target<arr[0]){
break;
}
if(arr[mid]==target){//如果中间位置等于目标元素,说明找到了将当前位置标记为索引,并结束循环
index =mid;
break;
}else if(arr[mid]>target){//如果中间位置大于目标元素,说明目标元素在前半部分,这时设置结束位置为中间位置-1,中间位置为(开始位置+结束位置)/2
end = mid-1;
mid = (begin+end)/2;
}else if(arr[mid]<target){//如果中间位置小于目标元素,说明目标元素在后半部分,这时设置开始位置为中间位置+1,中间位置为(开始位置+结束位置)/2
begin = mid;
mid = (begin+end)/2;
}
}
System.out.println("index:"+index);
}
栈
先进后出
队列
先进先出
单链表
单链表的基本使用(创建、查询、追加)
public class Node {
//节点内容
int data;
//下一个节点
Node next;
public Node(int data){
this.data = data;
}
//为节点追加节点
public Node append(Node node){
//当前节点
Node currentNode = this;
//循环向后找
while(true){
//取出下一个节点
Node nextNode = currentNode.next;
//如果没有下一个节点为null,当前节点已经是最后一个节点
if(nextNode==null){
break;
}
//赋给当前节点
currentNode = nextNode;
}
//把需要追加的节点追加到找到的当前节点的下一个节点
currentNode.next=node;
return this;
}
//获取下一个节点
public Node next(){
return this.next;
}
//获取节点中的数据
public int getData(){
return this.data;
}
//查看当前节点是否为最后一个节点
public boolean isLast(){
return next==null;
}
//显示节点信息
public void show(){
Node currentNode = this;
while(true){
System.out.println(currentNode.data+"");
//取出下一个节点
currentNode=currentNode.next;
//如果是最后一个节点的话
if(currentNode==null){
break;
}
}
}
}
单链表节点的删除
//删除下一个节点
public void removeNext(){
//取出当前节点的孙子节点
Node newNext = this.next.next;
//将当前节点的下一个节点指向孙子节点
this.next = newNext;
}
单链表节点的插入
//插入一个节点
public void after(Node node){
//取出下一个节点,作为孙子节点
Node nextNext = this.next;
//把新节点作为当前节点的下一个节点
this.next=node;
//把孙子节点设置为新节点的下一个节点
node.next = nextNext;
}
循环链表
和单链表类似,循环链表的最后一个节点要指向第一节点
循环链表初始化时,第一个节点的next要指向自己,也就是this
循环双链表
双链表的每个节点都记录着上一个节点和下一个节点
public class DoubleNode {
//上一个节点
DoubleNode previous=this;
//下一个节点
DoubleNode next=this;
//节点数据
int data;
public DoubleNode(int data){
this.data = data;
}
//增加节点
public void after(DoubleNode node){
//原来的下一个节点设置为孙子节点
DoubleNode nextNext = this.next;
//把新节点作为当前节点的下一个节点
this.next=node;
//把当前节点作为新节点的前一个节点
node.previous=this;
//把孙子节点设置为插入节点的下一个节点
node.next=nextNext;
//把孙子节点的前一个节点设置为插入的新节点
nextNext.previous=node;
}
//下一个节点
public DoubleNode next(){
return this.next;
}
//上一个节点
public DoubleNode previous(){
return this.previous;
}
public int getData(){
return this.data;
}
}
递归
**定义:**在一个方法的内部调用该方法本身的编程方式就叫做递归
**斐波那契数列:**从第三项开始,后面项等于前面两项之和
public static void main(String[] args) {
System.out.println(fenonacci(3));
}
//打印第N项斐波那契数列
public static int fenonacci(int i){
if(i==1 || i==2){
return 1;
}else{
return fenonacci(i-1)+fenonacci(i-2);
}
}
汉诺塔问题:
public static void main(String[] args) {
hanota(3,'A','B','C');
}
/**@param n 共有N个盘子
* @param from 开始的柱子
* @param in 中间的柱子
* @param to 目标柱子
*/
public static void hanota(int n,char from,char in,char to){
if(n==1){//所有的盘子都分为上面的所有盘子和下面的一个盘子,如果上面的盘子只有一个就
System.out.println("第一个盘子从"+from+"移到"+to);
}else{
//移动上面的所有盘子到中间位置
hanota(n-1,from,to,in);
//移动下面的盘子
System.out.println("第"+n+"个盘子从"+from+"移到"+to);
//移动上面的所有盘子从中间位置移动到目标位置
hanota(n-1,in,from,to);
}
}
3、排序算法
如何衡量一个算法的优劣
- 事后统计的方法
- 事前分析估算的方法
时间复杂度和空间复杂度概述
时间复杂度记为O(f(n))
常见的时间复杂度:
执行次数函数举例 | 阶 | 非正式术语 |
---|---|---|
12 | O(1) | 常数阶 |
2n+3 | O(n) | 线性阶 |
3n2+2n+1 | O(n2) | 平方阶 |
5log2n+20 | O(logn) | 对数阶 |
2n+3nlog2n+19 | O(nlogn) | nlogn阶 |
6n3+2n2+3n+4 | O(n3) | 立方阶 |
2n | O(2n) | 指数阶 |
时间复杂度从上到下,依次递增,越往下,算法的执行效率越低
**如何计算时间复杂度:**用常数1代替运行时间中的所有加法常数,修改后的运行次数函数中只保留最高项,去除最高阶项的系数(只看最高阶项,并且不看系数)
**平均时间复杂度:**加权平均值,期望值
**最坏时间复杂度:**在最糟糕的情况下,执行代码的时间复杂度。(一般讨论的是最坏时间复杂度)
空间复杂度是算法执行占用的内存空间
八种常用排序算法
交换排序
冒泡排序
算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
时间复杂度:
- 最好:O(n)
- 最坏O(n2)
public static void bubbleSort(int[] arr){
//外层循环控制比较多少轮
for(int i = 0; i<arr.length-1;i++){
//内层循环控制比较的次数
for(int j = 0;j<arr.length-i-1;j++){
if(arr[j]>arr[j+1]){
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
快速排序
算法步骤
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序
时间复杂度:
- 最好:O(nlog2n)
- 最坏:O(n2)
public static void QuickSort(int[] arr,int start,int end) {
if (start < end) {
//把数组中的第0个数字作为标准
int started = arr[start];
//记录需要排序的下标
int low = start;
int high = end;
//循环找比标准数大的数和比标准数小的数
while (low < high) {
//右边数字比标准数大,直接把下表向左移动
while (low < high && started <= arr[high]) {
high--;
}
//使用右边的数字替换左边的数
arr[low] = arr[high];
//如果左边数字比标准数字小
while (low < high && started >= arr[low]) {
low++;
}
arr[high] = arr[low];
}
//把标准数赋值给arr[low]或者arr[high]都可,因为它们重合了,指向同一个位置
arr[low] = started;
//处理所有小的数字,再处理所有大的数字
QuickSort(arr, start, low);
QuickSort(arr, low + 1, end);
}
}
插入排序
直接插入排序
算法步骤
- 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
- 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)
时间复杂度:
- 最好:O(n2)
- 最坏:O(n2)
public static void insertSort(int[] arr){
//遍历所有的数字
for(int i = 1; i<arr.length;i++){
//如果当前数字比后一个个数字小
if(arr[i]<arr[i-1]){
//把当前遍历数字存起来
int temp = arr[i];
int j;
//遍历当前数字前面的所有数字
for(j=i-1;j>=0&&arr[j]>temp;j--){
//把前一个数赋给后一个数字
arr[j+1]=arr[j];
}
//把临时变量for循环的当前元素,赋给不满足条件的后一个元素
arr[j+1] = temp;
}
}
}
希尔排序
算法步骤
- 选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
时间复杂度
- 最好:O(n)
- 最坏:O(n2)
比直接插入排序略好
public static void shellSort(int[] arr){
//遍历所有步长
for(int lang = arr.length/2;lang>0;lang/=2) {
//遍历所有元素
for (int i = lang; i < arr.length; i++) {
//遍历本组中所有元素
for (int j = i - lang; j >= 0; j -= lang) {
//如果当前元素大于加上步长后的元素,就进行交换
if (arr[j] > arr[j + lang]) {
int temp = arr[j];
arr[j] = arr[j + lang];
arr[j + lang] = temp;
}
}
}
}
}
选择排序
简单选择排序
算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
时间复杂度:
- 最好:O(n2)
- 最坏:O(n2)
//选择排序
private static void selectSort(int[] arr) {
//遍历所有的数
for(int i=0;i<arr.length;i++){
int minIndex=i;
for(int j=i+1;j<arr.length;j++){
if(arr[minIndex]>arr[j]){
//记录下最小的数的下标
minIndex=j;
}
}
//如果最小的数和当前遍历数的下标不一致
if(arr[minIndex]!=arr[i]){
int temp = arr[i];
arr[i]=arr[minIndex];
arr[minIndex]=temp;
}
}
}
堆排序
算法步骤
- 创建一个堆 H[0……n-1];
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并调用 shift_down(0),目的是把新的数组顶端数据调整到相应位置;
- 重复步骤 2,直到堆的尺寸为 1。
时间复杂度:
- 最好:O(nlog2n)
- 最坏:O(nlog2n)
public static void HeapSort(int[] arr) {
//开始位置是最后一个非叶子节点
int start = (arr.length-1)/2;
//结束位置,数组的长度-1
for(int i = start;i>=0;i--){
maxHeap(arr,arr.length,i);
}
//先把数组中的第0个和堆中的最后一个数交换位置,再把前面的处理为大顶堆
for(int i=arr.length-1;i>0;i--){
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;
//再次将数组排成大顶堆,略过数组第一个
maxHeap(arr,i,0);
}
}
//将数组调整为大顶堆
public static void maxHeap(int[] arr,int size,int index){
//左子节点
int leftNode = 2*index+1;
//右子节点
int rightNode = 2*index+2;
int max = index;
//和两个子节点分别对比,找出最大的节点
if(leftNode<size&&arr[leftNode]>arr[max]){
max=leftNode;
}
if(rightNode<size&&arr[rightNode]>arr[max]){
max=rightNode;
}
//交换位置
if(max!=index){
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
//交换位置后可能会破坏之前排好的堆,需要重新调整
maxHeap(arr,size,max);
}
}
归并排序
算法步骤
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
- 重复步骤 3 直到某一指针达到序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
时间复杂度:
- 最好:O(nlog2n)
- 最坏:O(nlog2n)
public static void mergeSort(int[] arr,int low,int high) {
int mid = (high+low)/2;
//如果起点大于终点说明已经不能再分了
if(low<high){
//处理左边
mergeSort(arr,low,mid);
//处理右边
mergeSort(arr,mid+1,high);
//归并
merge(arr,low,mid,high);
}
}
public static void merge(int[] arr,int low,int mid,int high){
// 用于存储归并后的临时数组
int[] temp = new int[high-low+1];
//用于记录在临时数组中存放的下标
int index=0;
//记录第一个数组中需要遍历的下标
int i =low;
//记录第二个数组中需要遍历的下标
int j =mid+1;
//遍历两个数组取出小的数字,放入临时数组中
while(i<=mid&&j<=high){
//如果第一个数字中的数据更小
if(arr[i]<=arr[j]){
//把第一个数组的元素放到临时数组中
temp[index]=arr[i];
//下标向后移动
i++;
}else{
temp[index]=arr[j];
j++;
}
index++;
}
//处理多余的数据
while(j<=high){
temp[index]=arr[j];
index++;
j++;
}
while(i<=mid){
temp[index]=arr[i];
index++;
i++;
}
//把临时数组放回原数组
for(int k=0;k<temp.length;k++){
arr[k+low]=temp[k];
}
基数排序
算法步骤
- 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零
- 从最低位开始,依次进行一次排序
- 从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
时间复杂度:(d代表数字长度,r代表数字的基数)
- 最好:O(d(n+rd))
- 最坏:O(d(r+n))
public static void radixSort(int[] arr){
//存数组中最大的数字
int max = Integer.MIN_VALUE;
for(int i=0;i<arr.length;i++){
if(arr[i]>max){
max=arr[i];
}
}
//求最大数字的位数
int maxLength = (max+"").length();
//用于临时存储数据的数组
int[][] temp = new int[10][arr.length];
//用于记录在相应的数组中存放数字的数量
int[] counts = new int[10];
//根据最大长度决定比较的次数
System.out.println(maxLength);
for(int i=0,n = 1;i<maxLength;i++,n*=10){
// 把每个数字分别计算余数
for(int j = 0;j<arr.length;j++){
//计算余数
int ys = arr[j] / n % 10;
//把当前遍历的数据放入指定的数组中
temp[ys][counts[ys]] = arr[j];
//记录数量
counts[ys]++;
}
//记录取的元素需要放的位置
int index = 0;
//把数字取出来
for(int k=0;k<counts.length;k++){
if(counts[k]!=0){
for(int m=0;m<counts[k];m++){
//取出元素
arr[index] = temp[k][m];
index++;
}
//把数量置空
counts[k]=0;
}
}
}
}
基数排序之队列实现
public static void radixSort(int[] arr){
//存数组中最大的数字
int max = Integer.MIN_VALUE;
for(int i=0;i<arr.length;i++){
if(arr[i]>max){
max=arr[i];
}
}
//求最大数字的位数
int maxLength = (max+"").length();
System.out.println(maxLength);
//用于临时存储数据的队列
MyQueue[] temp = new MyQueue[10];
//为队列数组赋值
for(int i = 0;i<temp.length;i++){
temp[i]=new MyQueue();
}
//用于记录在相应的数组中存放数字的数量
int[] counts = new int[10];
//根据最大长度决定比较的次数
for(int i=0,n = 1;i<maxLength;i++,n*=10){
// 把每个数字分别计算余数
for(int j = 0;j<arr.length;j++){
//计算余数
int ys = arr[j] / n % 10;
//把当前遍历的数据放入指定的队列中
temp[ys].put(arr[j]);
}
//记录取的元素需要放的位置
int index = 0;
//把所有队列中的数字取出来
for(int k=0;k<temp.length;k++){
//当前遍历的队列不为空
while(!temp[k].isEmpty()){
//取出元素
arr[index] = temp[k].get();
//记录下一个位置
index++;
}
}
}
}
八种排序算法的对比
4、树结构
数结构概述
什么是树结构
为什么使用树结构
树的基本概念
-
跟节点
-
双亲节点
-
子节点
-
路径
节点与节点之间的连线
-
节点的度
节点的子节点的个数
-
节点的权
节点上的数值
-
叶子节点
-
子树
-
层
-
树的高度
-
森林
二叉树
什么是二叉树
任何一个节点的子节点的数量不超过2,二叉树的子节点分左节点和右节点,不能随意交换。
满二叉树
所有叶子节点都在最后一层,而且节点的总数为2^n-1(n是树的高度)
完全二叉树
所有叶子节点都在最后一层,或倒数第二层且最有一层的叶子节点在左边连续,倒数第二层的节点在右边连续
链式存储的二叉树
创建一个二叉树:
//二叉树类
public class BinaryTree {
TreeNode root;
//设置根节点
public TreeNode getRoot() {
return root;
}
//获取根节点
public void setRoot(TreeNode root) {
this.root = root;
}
}
//节点类
public class TreeNode {
//节点的权
int value;
//左儿子
TreeNode leftNode;
//右儿子
TreeNode rightNode;
public TreeNode(int value){
this.value=value;
}
//设置左子节点
public void setlNode(TreeNode leftNode) {
this.leftNode = leftNode;
}
//设置右子节点
public void setrNode(TreeNode rightNode) {
this.rightNode = rightNode;
}
}
//将节点连接到树
public static void main(String[] args) {
BinaryTree binaryTree = new BinaryTree();
//创建根节点
TreeNode root = new TreeNode(1);
//把跟节点赋给树
binaryTree.setRoot(root);
//创建两个节点
TreeNode rootL = new TreeNode(2);
TreeNode rootR = new TreeNode(3);
//设置两个节点分别为root节点的左子节点和右子节点
root.setlNode(rootL);
root.setlNode(rootR);
}
二叉树的遍历:
//二叉树类
public void frontShow() {
root.frontShow();
}
public void midShow() {
root.midShow();
}
public void afterShow() {
root.afterShow();
}
//节点类
//前序遍历
public void frontShow() {
//先遍历当前节点
System.out.println(value);
//遍历左节点
if(leftNode!=null){
leftNode.frontShow();
}
//遍历右节点
if(rightNode!=null){
rightNode.frontShow();
}
}
//中序遍历
public void midShow() {
//遍历左节点
if(leftNode!=null){
leftNode.midShow();
}
//先遍历当前节点
System.out.println(value);
//遍历右节点
if(rightNode!=null){
rightNode.midShow();
}
}
//后序遍历
public void afterShow() {
//遍历左节点
if(leftNode!=null){
leftNode.afterShow();
}
//遍历右节点
if(rightNode!=null){
rightNode.afterShow();
}
//先遍历当前节点
System.out.println(value);
}
二叉树节点的查找
//节点类
public TreeNode frontSearch(int i) {
TreeNode target=null;
//对比当前节点的值
if(this.value==i){
return this;
//当前节点的值不是要查找的节点
}else {
//查找左子节点
if(leftNode!=null){
//有可能查到,也可能查不到,如果查不到,返回空
target = leftNode.frontSearch(i);
}
//如果不为空,说明在左子节点中找到
if(target!=null){
return target;
}
//查找右子节点
if(rightNode!=null){
target = rightNode.frontSearch(i);
}
}
return target;
}
//二叉树类
public TreeNode frontSearch(int i) {
return root.frontSearch(i);
}
//测试类
//前序查找
TreeNode result = binaryTree.frontSearch(5);
System.out.println(result.getValue());
二叉树节点的删除
//二叉树类
//如果要删除的节点是根节点就把跟节点置空,相当于删除整棵树
public void delete(int i) {
if(root.value==i){
root=null;
}else {
root.delete(i);
}
}
//节点类
//删除子树
public void delete(int i) {
TreeNode parent = this;
//判断左子节点
if(parent.leftNode!=null&&parent.leftNode.value==i){
parent.leftNode=null;
return;
}
//判断右子节点
if(parent.rightNode!=null&&parent.rightNode.value==i){
parent.rightNode=null;
return;
}
//递归检查并删除左子节点
parent=leftNode;
if(parent!=null){
parent.delete(i);
}
//递归检查并删除右子节点
parent=rightNode;
if(parent!=null){
parent.delete(i);
}
}
//测试类
//删除节点(子树)
binaryTree.delete(1);
//展示删除节点后的二叉树
binaryTree.frontShow();
顺序存储的二叉树
通常情况只考虑完全二叉树
顺序存储二叉树的性质
-
对于第n个元素的左子节点是:2*n+1
-
对于第n个元素的右子节点是:2*n+2
-
对于第n个元素的父节点是:(n-1)/2
遍历(把数组作为二叉树遍历)
public void frontShow(){
frontShow(0);
}
//前序遍历
public void frontShow(int index){
if(data==null||data.length==0){
return;
}
//先遍历当前节点的内容
System.out.print(data[index]+",");
//2*index+1:处理左子树
if(2*index+1<data.length){
frontShow(2*index+1);
}
//2*index+2:处理右子树
if(2*index+2<data.length){
frontShow(2*index+2);
}
}
//测试类
public static void main(String[] args) {
int[] data = new int[]{1,2,3,4,5,6,7};
ArrayBinaryTree tree = new ArrayBinaryTree(data);
tree.frontShow();
}
线索二叉树
什么是线索二叉树
线索化二叉树时,一个节点的前一个节点,叫前驱节点
线索化二叉树时,一个节点的后一个节点,叫后继节点
线索二叉树遍历-------
赫夫曼树
赫夫曼树概述
最优二叉树
它是n个带权叶子节点构成的所有二叉树中,带权路径最长度最小的二叉树
节点的带权路径
节点的权值*根节点到该节点的路径
树的带权路径长度WPL
树的带权路径长度WPL:树中所有叶子节点的带权路径长度之和
赫夫曼树代码实现
流程分析:
- 排序
- 取出跟节点权值最小的两个二叉树
- 组成新的二叉树,前面取出的两个二叉树是新二叉树的子树
- 新二叉树的跟节点的权值等于其子节点权值之和
public static Node createHuffmanTree(int[] arr){
//先使用数组中所有的元素创建若干个二叉树,(只有一个节点)
List<Node> nodes = new ArrayList<>();
for(int value:arr){
nodes.add(new Node(value));
}
while(nodes.size()>1){
//排序
Collections.sort(nodes);
//取出权值最小的两个二叉树
Node left = nodes.get(nodes.size() - 1);
Node right = nodes.get(nodes.size() - 2);
//创建新的二叉树
Node parent = new Node(left.value+right.value);
//把取出来的两个二叉树移除
nodes.remove(left);
nodes.remove(right);
//放回原的二叉树集合中
nodes.add(parent);
}
return nodes.get(0);
}
//节点类
public class Node implements Comparable<Node>{
int value;
Node left;
Node right;
public Node(int value) {
this.value = value;
}
@Override
public int compareTo(Node o) {
return -(this.value-o.value);
}
@Override
public String toString() {
return "Node{" +
"value=" + value +
'}';
}
}
赫夫曼编码概述
赫夫曼编码代码实现
public class TestHuffmanCode {
public static void main(String[] args) {
String msg = "can you can a can as a can canner can a can";
byte[] bytes = msg.getBytes();
//进行赫夫曼编码压缩
byte[] b = huffmanZip(bytes);
System.out.println(bytes.length);
System.out.println(b.length);
}
//用于临时存储路径
static StringBuilder sb = new StringBuilder();
//用于存储赫夫曼编码
static Map<Byte,String> huffCodes = new HashMap<>();
private static byte[] huffmanZip(byte[] bytes) {
//先统计每个byte出现的次数,并放入一个集合中
List<Node> nodes = getNodes(bytes);
//创建一个棵赫夫曼树
Node tree = createHuffmanTree(nodes);
System.out.println(tree);
//创建一个赫夫曼编码表
Map<Byte,String> huffmanTable = getCodes(tree);
//编码
byte[] b = zip(bytes,huffmanTable);
return b;
}
/**
* 进行赫夫曼编码
* @param bytes
* @param huffmanTable
* @return
*/
private static byte[] zip(byte[] bytes, Map<Byte, String> huffmanTable) {
StringBuilder sb = new StringBuilder();
for(byte b:bytes){
sb.append((huffmanTable.get(b)));
}
// System.out.println(sb.toString());
int len;
if(sb.length()%8==0){
len=sb.length()/8;
}else{
len=sb.length()/8+1;
}
//用于存储压缩后的byte
byte[] by = new byte[len];
//记录新的byte的位置
int index = 0;
for(int i=0;i<sb.length();i+=8){
String strByte;
if(i+8>sb.length()){
strByte = sb.substring(i);
}else{
strByte = sb.substring(i, i + 8);
}
System.out.println(strByte);
byte byt = (byte)Integer.parseInt(strByte,2);
//System.out.println(strByte+":"+byt);
by[index]=byt;
index++;
}
return by;
}
//创建赫夫曼树
private static Node createHuffmanTree(List<Node> nodes) {
while (nodes.size()>1){
// //排序
Collections.sort(nodes);
System.out.println(nodes);
//取出两个权值最低的二叉树
Node left = nodes.get(nodes.size() - 1);
Node right = nodes.get(nodes.size() - 2);
//创建一个新的二叉树
Node parent = new Node(null,left.weight+right.weight);
//把之前取出来的两棵二叉树设置为新创建的二叉树的子树
parent.left=left;
parent.right=right;
//把前面取出来的两棵二叉树删除
nodes.remove(left);
nodes.remove(right);
//把新创建的二叉树放回原的二叉树集合中
nodes.add(parent);
}
return nodes.get(0);
}
/**
* 把byte数组转换为node集合
* @param bytes
* @return
*/
private static List<Node> getNodes(byte[] bytes) {
List<Node> nodes = new ArrayList<>();
//counts为每个键盘输入的值的次数
Map<Byte,Integer> counts = new HashMap<>();
//统计每个byte出现的次数
for(byte b:bytes){
Integer count = counts.get(b);
if(count == null){
counts.put(b,1);
}else {
counts.put(b,count+1);
}
}
//把每个键值对转换为一个node对象
for(Map.Entry<Byte,Integer> entry:counts.entrySet()){
nodes.add(new Node(entry.getKey(),entry.getValue()));
}
return nodes;
}
//根据赫夫曼树获取赫夫曼编码表
private static Map<Byte, String> getCodes(Node tree) {
if(tree==null){
return null;
}
getCodes(tree.left,"0", sb);
getCodes(tree.right,"1", sb);
return huffCodes;
}
private static void getCodes(Node node, String code, StringBuilder sb) {
StringBuilder sb2 = new StringBuilder(sb);
sb2.append(code);
if(node.data==null){
getCodes(node.left,"0",sb2);
getCodes(node.right,"1",sb2);
}else {
huffCodes.put(node.data,sb2.toString());
}
}
}
赫夫曼编码的解码
//使用赫夫曼编码进行解码
byte[] newBytes = decode(huffCodes,b);
System.out.println(new String(newBytes));
//使用指定的赫夫曼编码表进行解码
private static byte[] decode(Map<Byte, String> huffCodes, byte[] bytes) {
StringBuilder sb = new StringBuilder();
//把byte数组转为一个二进制字符串
for(int i = 0;i<bytes.length;i++){
byte b = bytes[i];
boolean flag = (i==bytes.length-1);
sb.append(byteToBitStr(!flag,b));
}
System.out.println(sb.toString());
//把字符串按照指定的赫夫曼编码进行解码
//把赫夫曼编码的键值对进行调换
Map<String,Byte> map = new HashMap<>();
for(Map.Entry<Byte,String> entry:huffCodes.entrySet()){
map.put(entry.getValue(),entry.getKey());
}
//创建一个集合,用于存byte
List<Byte> list = new ArrayList<>();
//处理字符串
for(int i=0;i<sb.length();){
int count=1;
boolean flag = true;
Byte b = null;
//截取出一个byte
while(flag){
String key = sb.substring(i,i+count);
b = map.get(key);
if(b==null){
count++;
}else{
flag = false;
}
}
list.add(b);
i+=count;
}
// System.out.println(list);
//把集合转为数组
byte[] b = new byte[list.size()];
for(int i=0;i<b.length;i++){
b[i]=list.get(i);
}
return b;
}
private static String byteToBitStr(boolean flag,byte b){
int temp = b;
if(flag){
temp|=256;
}
String str = Integer.toBinaryString(temp);
if(flag){
return str.substring(str.length()-8);
}else{
return str;
}
}
赫夫曼编码文件的压缩与解压
//调用文件压缩
String src = "32.png";
String dst = "33.zip";
try{
zipFile(src,dst);
}catch (Exception e){
e.printStackTrace();
}
//调用解压文件
String src = "33.zip";
String dst = "44.JPG";
try {
unZip(src,dst);
} catch (Exception e) {
e.printStackTrace();
}
/**
* 文件的解压
* @param src
* @param dst
*/
public static void unZip(String src,String dst) throws IOException, ClassNotFoundException {
//读取文件数据,创建一个输入流
InputStream is = new FileInputStream(src);
ObjectInputStream ois = new ObjectInputStream(is);
//读取byte数组
byte[] b = (byte[]) ois.readObject();
//读取赫夫曼编码表
Map<Byte,String> codes = (Map<Byte, String>) ois.readObject();
ois.close();
is.close();
//解码
byte[] bytes = decode(codes, b);
//创建一个输出流
OutputStream os = new FileOutputStream(dst);
//写出数据
os.write(bytes);
os.close();
}
/**
* 压缩文件
* @param src 文件路径
* @param dst 输出路径
*/
public static void zipFile(String src,String dst) throws IOException {
//创建一个输入流
InputStream is = new FileInputStream(src);
//创建一个和输入流指向的文件大小一样的byte数组
byte[] b = new byte[is.available()];
//读取文件内容
is.read(b);
is.close();
//使用赫夫曼编码进行编码
byte[] ByteZip = huffmanZip(b);
System.out.println(b.length);
System.out.println(ByteZip.length);
//输出流
OutputStream os = new FileOutputStream(dst);
ObjectOutputStream oos = new ObjectOutputStream(os);
//把压缩后的byte数组写入文件
oos.writeObject(ByteZip);
//把赫夫曼编码表写入文件
oos.writeObject(huffCodes);
oos.close();
os.close();
}
二叉排序树
什么是二叉排序树:
也叫二叉查找树,对于二叉树中的任何一个非叶子节点,要求左子节点比当前节点值小,右子节点比当前节点值大。如果是空树,可以认为是一个二叉排序树
创建二叉排序树
//树类
/**
* 向二叉排序树中添加节点
* @param node
*/
public void add(Node node){
//如果是一颗空树
if(root==null){
root=node;
}else{
root.add(node);
}
}
//中序遍历二叉排序树,就是从小到大
public void midShow(){
if(root!=null){
root.midShow(root);
}
}
//Node节点类
//向子树中添加节点
public void add(Node node){
if(node==null){
return;
}
//判断传入的节点值比当前子树的跟节点的值大还是小
//如果添加的节点比当前节点值更小
if(node.value<this.value){
//如果左节点为空
if(this.left==null){
this.left=node;
}else{
this.left.add(node);
}
}else{
if(this.right==null){
this.right=node;
}else{
this.right.add(node);
}
}
}
//中序遍历
public void midShow(Node node) {
if(node==null) {
return;
}
midShow(node.left);
System.out.println(node.value);
midShow(node.right);
}
//测试方法
public static void main(String[] args) {
int[] arr = new int[]{7,3,10,12,5,1,9};
//创建一颗二叉排序树
BinarySortTree bst = new BinarySortTree();
//循环添加
for(int i:arr){
bst.add(new Node(i));
}
//中序遍历二叉树
bst.midShow();
}
查找节点
//查找节点
public Node search(int value){
if(root==null){
return null;
}else{
return root.search(value);
}
}
//查找节点
public Node search(int value) {
if (this.value==value) {
return this;
}else if(value<this.value){
if(left==null){
return null;
}
return left.search(value);
}else {
if(right==null){
return null;
}
return right.search(value);
}
}
删除叶子节点
//删除叶子节点
public void delete(int value){
if(root==null){
return;
}else{
//找到节点
Node target = search(value);
//如果没有这个节点,直接返回
if(target==null){
return;
}
//找到该节点的父节点
Node parent = searchParent(value);
//要删除的节点是叶子节点
if(target.left==null&&target.right==null){
//要删除的节点是父节点的左子节点
if(parent.left.value==target.value){
parent.left=null;
}else {
parent.right=null;
}
//要删除的节点是父节点的右子节点
}
}
}
//搜索父节点
public Node searchParent(int value){
if (root==null){
return null;
}else {
return root.searchParent(value);
}
}
//搜索父节点
public Node searchParent(int value) {
if((this.left!=null&&this.left.value==value)||(this.right!=null&&this.right.value==value)){
return this;
}else {
if(this.value>value&&this.left!=null){
return this.left.searchParent(value);
}else if(this.value<value&&this.right!=null){
return this.right.searchParent(value);
}
}
return null;
}
删除有一个子节点的节点
//要删除的节点有一个左子节点或右子节点
if(target.left!=null){//有左子节点
//要删除的节点是父节点的左子节点
if (parent.left.value == target.value) {
parent.left = target.left;
} else {
//要删除的节点是父节点的右子节点
parent.right = target.left;
}
}else {//有右子节点
if (parent.right.value == target.value) {
parent.left = target.right;
} else {
//要删除的节点是父节点的右子节点
parent.right = target.right;
}
}
删除有两个子节点的节点
思想:找到目标节点的后继节点,拿到该后继节点的值,并删掉该节点(后继节点是目标节点右子树中权值最小的节点)
//要删除的节点有两个节点
else if (target.left != null && target.right != null) {
//删除右子树中值最小的节点,获取到该节点的值
int min = deleteMin(target.right);
//替换目标节点中的值
target.value=min;
}
//删除一棵树中最小的节点
private int deleteMin(Node node) {
Node target = node;
//递归向左找,找到最小的
while(target.left!=null){
target=target.left;
}
//删除最小的节点
delete(target.value);
return target.value;
}
AVL树
AVL树概述
平衡二叉树:左子树和右子树的高度差的绝对值不超过1,左子树和右子树也是平衡二叉树
单旋转
右旋转
步骤:
- 创建一个新节点,值等于当前节点的值
- 将当前节点的右子节点等于新节点的右子节点
- 把新节点的左子树设置为当前节点的左子树的右子树
- 把当前节点的值换为左子节点的值
- 把当前节点的左子树设置为左子树的左子树
- 把当前节点的右子树设置为新节点
private void rightRotate() {
//创建一个新节点,值等于当前节点的值
Node newRight = new Node(value);
//把新节点的右子树设置为当前节点的右子树
newRight.right = right;
//把新节点的左子树设置为当前节点的左子树的右子树
newRight.left = left.right;
//把当前节点的值换为左子节点的值
value=left.value;
//把当前节点的左子树设置为左子树的左子树
left=left.left;
//把当前节点的右子树设置为新节点
right=newRight;
}
右旋转
步骤与左旋转一样,方向相反
//右旋转
private void rightRotate() {
//创建一个新节点,值等于当前节点的值
Node newRight = new Node(value);
//把新节点的右子树设置为当前节点的右子树
newRight.right = right;
//把新节点的左子树设置为当前节点的左子树的右子树
newRight.left = left.right;
//把当前节点的值换为左子节点的值
value=left.value;
//把当前节点的左子树设置为左子树的左子树
left=left.left;
//把当前节点的右子树设置为新节点
right=newRight;
}
双旋转
//进行右旋转
if(leftHeight()-rightHeight()>=2){
//双旋转
if(left.leftHeight()<left.rightHeight()&&left!=null){
left.leftRotate();
rightRotate();
}
//单旋转
rightRotate();
}
//进行左旋转
if (leftHeight()-rightHeight()<=-2){
//双旋转
if(right!=null&&leftHeight()>rightHeight()){
right.rightRotate();
}
//单旋转
leftRotate();
}
多路查找树
计算机的存储
2-3树2-3-4树
2-3树中有两个子节点的节点成为2节点,有三个子节点的节点叫3节点,2节点要么有两个子节点,要么没有子节点,3节点要么有三个子节点,要么没有子节点
B树,B+树
B树中最大的节点的子节点树成为B树的阶
B+树的非叶节点只存储索引,不存储数据
B+树的所有的叶节点组成了一个有序链表
5、哈希表
哈希表概述
设计:计算简单,分布均匀
散列函数
直接定址法
直接把关键字作为地址
数字分析法
使用数字分析法要事先知道数据的规律
平方取中法
计算数值的平方,取中间的值插入数组中
取余法
随机数法
散列函数冲突的解决方案
开放地址法
- 线性探测法
- 二次探测法
- 再哈希法
链地址法(用得更多)
6、图结构
基本概念
图结构
临接
路径
有向图和无向图
带权图
创建图结构
/**
* 顶点类
* @author lee
* @date 2020/8/24 - 4:29 下午
*/
public class Vertex {
private String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Vertex(String value) {
this.value = value;
}
}
/**
* 图类
* @author lee
* @date 2020/8/24 - 4:30 下午
*/
public class Graph {
private Vertex[] vertex;
private int currentSize;
public int[][] adjMat;
public Graph(int size) {
vertex = new Vertex[size];
adjMat = new int[size][size];
}
//向图中加入一个定点
public void addVertex(Vertex v){
vertex[currentSize++]=v;
}
public void addEdge(String v1,String v2){
//找出两个定点的下标
int index1 = 0;
for(int i = 0;i<vertex.length;i++){
if(vertex[i].getValue().equals(v1)){
index1=i;
break;
}
}
int index2 = 0;
for(int i = 0;i<vertex.length;i++){
if(vertex[i].getValue().equals(v2)){
index2=i;
break;
}
}
adjMat[index1][index2]=1;
adjMat[index2][index1]=1;
}
}
//测试
public static void main(String[] args) {
Vertex v1 = new Vertex("A");
Vertex v2 = new Vertex("B");
Vertex v3 = new Vertex("C");
Vertex v4 = new Vertex("D");
Vertex v5 = new Vertex("E");
Graph graph = new Graph(5);
graph.addVertex(v1);
graph.addVertex(v2);
graph.addVertex(v3);
graph.addVertex(v4);
graph.addVertex(v5);
//增加边
graph.addEdge("A","C");
graph.addEdge("B","C");
graph.addEdge("A","B");
graph.addEdge("B","D");
graph.addEdge("B","E");
for(int[] a: graph.adjMat){
System.out.println(Arrays.toString(a));
}
}