赞
踩
断断续续看了一个多月,终于看完了,老师讲的很好,看完收获满满。
视频地址:https://www.bilibili.com/video/BV1Zt411o7Rn?p=1
这是代码和思维导图
链接:https://pan.baidu.com/s/1hybDPGenDnFHWbszdEmUaA
提取码:i26u
什么是数据结构?
- 数据结构是指相互存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成。
- 白话:数据与数据之间的关系
数据结构主要学什么?
- 数据的存储结构:数据在计算机内存是什么方式存储的
- 顺序存储结构
- 链式存储结构
- 数据的逻辑结构:数据与数据之间是什么关系
- 集合结构:集合结构中的数据同属于一个集合,他们之间是并列的关系,除此之外没有其他的关系。
- 线性结构:线性结构中的元素存在一对一的相互关系。
- 树形结构:树形结构中的元素存在一对多的相互关系。
- 图形结构:树形结构中的元素存在多对多的相互关系。
什么是算法?
- 是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述问题的策略机制。
- 白话:解决问题的思路
算法的特性
- 输入:可以为算法提供0到多个输入
- 输出:每个算法至少有一个输出
- 有穷性:算法在有限的时间得出结果
- 确定性:一个结果对应一个输出
- 可行性:算法能解决实际问题
算法的基本要求
- 正确性:能正确的解决问题
- 可读性:算法有一定的可读性
- 健壮性:对不同的输入都有相应的反应,不合法的输入要给出相应的提示输出
- 时间复杂度:算法要占用的时间
- 空间复杂度:算法运行占用的内存
数组(Array) 是一种很常见的数据结构。它由相同类型的元素(element)组成,并且是使用一块连续的内存来存储。
我们直接可以利用元素的索引(index)可以计算出该元素对应的存储地址。
数组的特点是:提供随机访问 并且容量有限。
数组的基本使用
- 数组的创建,获取数组长度,数组元素赋值,遍历数组
数组元素的添加
- 创建一个新的数组,复制原数组,再把目标元素添加到新数组
数组元素的删除
- 创建一个新的数组来接收
面向对象的数组
- 把数组当成一个对象创建
- 在数组类中定义增删改的方法(感觉像在写简单的arrayList源码)
public class MyArray { //用于存储的数组 private int[] elements; public MyArray() { elements = new int[0]; } //获取数组长度方法 public int size() { return elements.length; } //往数组的末尾添加一个元素 public void add(int element) { //创建一个新的数组 int[] newArr = new int[elements.length+1]; //把原数组的元素复制到新数组中 for (int i = 0; i < elements.length; i++) { newArr[i] = elements[i]; } //把添加的元素放入新数组中 newArr[elements.length] = element; //使用新数组代替旧数组 elements = newArr; } //打印数组所有元素到控制台 public void show() { System.out.println(Arrays.toString(elements)); } //删除数组中的元素 public void delete(int index) { //判断下标是否越界 if(index < 0 || index > elements.length - 1) { throw new RuntimeException("下标越界"); } //创建一个新的数组,长度为原数组的长度减1 int[] newArr = new int[elements.length - 1]; //复制原有数组到新数组 for (int i = 0; i < newArr.length; i++) { //想要删除元素前面的元素 if(index < i) { newArr[i] = elements[i]; //想要删除元素后面的元素 }else{ newArr[i] = elements[i + 1]; } } //新数组替代旧数组 elements = newArr; } //获取某个元素 public int get(int index) { return elements[index]; } //插入元素到指定位置 public void insert(int index,int element) { //创建一个新的数组 int[] newArr = new int[element + 1]; //将原数组复制到新数组中 for (int i = 0; i < newArr.length; i++) { if(i < index) { newArr[i] = elements[i]; }else { newArr[i + 1] = elements[i]; } } //插入新的元素 elements[index] = element; //新数组替代旧数组 elements = newArr; } }
public class TestSearch { public static void main(String[] args) { //目标数组 int[] arr = new int[] {9,8,7,6,5,4,3}; //目标元素 int element = 8; //目标元素所在下标 int index = -1; //遍历数组 for (int i = 0; i < arr.length; i++) { if(arr[i] == element) { index = i; break; } } //打印目标元素的下标 System.out.println("index:" + index); } }
二分查找的前提:被查找的目标数组必须是有序数组
实现思路:把数组切成两半,从中间开始找
public class TestBinarySearch { public static void main(String[] args) { //目标数组 int[] arr = new int[] {1,2,3,4,5,6,7,8,9}; //目标元素 int element = 2; //记录开始的位置 int begin = 0; //记录结束的位置 int end = arr.length - 1; //记录中间的位置 int mid = (begin+end) / 2; //记录目标下标 int index = -1; //循环查找 while(true) { //判断中间这个元素是否是要查找的元素 if(arr[mid] == element) { index = mid; break; //中间这个元素不是要查找的元素 }else { if(arr[mid] > element) { //把结束位置调整到中间位置减一 end = mid - 1; //中间元素比目标元素小 }else { //把开始位置调整到中间位置加一 begin = mid + 1; } //取出新的中间位置 mid = (begin + end) / 2; } } System.out.println("index: " + index); } }
栈 (stack)只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 后进先出(LIFO, Last In First Out) 的原理运作。在栈中,push 和 pop 的操作都发生在栈顶。
栈常用一维数组或链表来实现,用数组实现的栈叫作 顺序栈 ,用链表实现的栈叫作 链式栈 。
以下代码写了
栈
的基本操作
public class MyStack { //栈的底层我们用数组存储元素 int[] elements; public MyStack() { elements = new int[0]; } //压入元素 public void push(int element) { //创建一个新的数组 int[] newArr = new int[elements.length+1]; //把原数组的元素复制到新数组中 for (int i = 0; i < elements.length; i++) { newArr[i] = elements[i]; } //把添加的元素放入新数组中 newArr[elements.length] = element; //使用新数组代替旧数组 elements = newArr; } //取出栈顶元素 public int pop() { //栈中没有元素 if (elements.length == 0) { throw new RuntimeException("stack is empty"); } //取出数组的最后一个元素 int element = elements[elements.length - 1]; //创建一个新的数组 int[] newArr = new int[elements.length - 1]; //原数组中除了最后一个元素的其他元素都放入新的数组 for (int i = 0; i < newArr.length - 1; i++) { newArr[i] = elements[i]; } //替换数组 elements = newArr; //返回栈顶元素 return element; } //查看栈顶元素 public int peek() { return elements[elements.length - 1]; } //判断栈是否为空 public boolean isEmpty() { return elements.length == 0; } }
队列 是 先进先出( FIFO,First In, First Out) 的线性表。在具体应用中通常用链表或者数组来实现,用数组实现的队列叫作 顺序队列 ,用链表实现的队列叫作 链式队列 。队列只允许在后端(rear)进行插入操作也就是 入队 enqueue,在前端(front)进行删除操作也就是出队 dequeue
队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。
以下代码写了
队列
的基本操作
public class MyQueue { int[] elements; public MyQueue() { elements = new int[0]; } //入队 public void add(int element) { //创建一个新的数组 int[] newArr = new int[elements.length + 1]; //把原数组的元素复制到新数组中 for (int i = 0; i < elements.length; i++) { newArr[i] = elements[i]; } //把添加的元素放入新数组中 newArr[elements.length] = element; //使用新数组代替旧数组 elements = newArr; } //出队 public int poll() { //把数组中的第0个元素取出来 int element = elements[0]; //创建一个新的数组 int[] newArr = new int[elements.length - 1]; //复制原数组中的新数组中 for (int i = 0; i < newArr.length; i++) { newArr[i] = elements[i + 1]; } //替代数组 elements = newArr; return element; } //判断队列是否为空 public Boolean isEmpty() { return elements.length == 0; } }
链表(LinkedList) 虽然是一种线性表,但是并不会按线性的顺序存储数据,使用的不是连续的内存空间来存储数据。
链表的插入和删除操作的复杂度为 O(1) ,只需要知道目标位置元素的上一个元素即可。但是,在查找一个节点或者访问特定位置的节点的时候复杂度为 O(n) 。
使用链表结构可以克服数组需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但链表不会节省空间,相比于数组会占用更多的空间,因为链表中每个节点存放的还有指向其他节点的指针。除此之外,链表不具有数组随机读取的优点。
单链表 单向链表只有一个方向,结点只有一个后继指针 next 指向后面的节点。因此,链表这种数据结构通常在物理内存上是不连续的。我们习惯性地把第一个结点叫作头结点,链表通常有一个不保存任何值的 head 节点(头结点),通过头结点我们可以遍历整个链表。尾结点通常指向 null。
以下写了单链表的基本操作
//一个节点 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 class TestNode {
public static void main(String[] args) {
//创建节点
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
//追加节点
n1.append(n2).append(n3).append(new Node(4));
//取出下一个节点
System.out.println(n1.next().next().next().getData());
//判断节点是否为空
System.out.println(n1.isLast());
System.out.println(n1.next().next().next().isLast());
}
节点的删除
//显示所有节点信息 public void show() { Node currentNode = this; while(true) { System.out.print(currentNode.data + " "); //取出下一个节点 currentNode = currentNode.next; //判断是否是最后一个 if(currentNode == null) { break; } } } //删除下一个节点 public void removeNode() { //取出下下个节点 Node nextNode = next.next; //把下下个节点设置为当前节点的下一个节点 this.next = nextNode; }
插入节点
//插入一个节点作为当前节点的下一个节点 public void after(Node node) { //取出下一个节点,作为下下个节点 Node nextnextNode = next; //把新节点作为当前节点的下一个节点 this.next = node; //把下下个节点设置为新节点的下一个节点 node.next = nextnextNode; } //Demo代码 public class TestNode { public static void main(String[] args) { //创建节点 Node n1 = new Node(1); Node n2 = new Node(2); Node n3 = new Node(3); //追加节点 n1.append(n2).append(n3).append(new Node(4)); //取出下一个节点 //System.out.println(n1.next().next().next().getData()); //判断节点是否为空 //System.out.println(n1.isLast()); //System.out.println(n1.next().next().next().isLast()); n1.show(); Node n5 = new Node(5); n1.next().after(n5); n1.show(); } }
循环链表 其实是一种特殊的单链表,和单链表不同的是循环链表的尾结点不是指向 null,而是指向链表的头结点。
//一个节点 public class LoopNode { //节点内容 int data; //下一个节点 LoopNode next = this; //创建一个构造方法 public LoopNode(int data) { this.data = data; } //获取下一个节点 public LoopNode next() { return this.next; } //获取节点中的数据 public int getData() { return this.data; } //插入一个节点作为当前节点的下一个节点 public void after(LoopNode node) { //取出下一个节点,作为下下个节点 LoopNode nextnextNode = next; //把新节点作为当前节点的下一个节点 this.next = node; //把下下个节点设置为新节点的下一个节点 node.next = nextnextNode; } //删除下一个节点 public void removeNode() { //取出下下个节点 LoopNode nextNode = next.next; //把下下个节点设置为当前节点的下一个节点 this.next = nextNode; } }
每一个节点都可以找其节点的上一个节点和下一个节点,因为是循环链表,所有没有最后一个节点
public class DoubleNode { //上一个节点 DoubleNode pre = this; //下一个节点 DoubleNode next = this; //节点信息 int data; //构造方法 public DoubleNode(int data) { this.data = data; } //增加节点 public void after(DoubleNode node) { //当前节点的下一个节点 DoubleNode nextnextNode = next; //把新节点作为当前节点的下节点 this.next = node; //把当前节点作为新节点的上节点 node.pre = this; //把新节点的下节点指向当前节点的下节点 node.next = nextnextNode; //把当前节点的下一个节点的上节点指向新节点 nextnextNode.pre = node; } //下一个节点 public DoubleNode next() { return this.next; } public int getData() { return this.data; } //上一个节点 public DoubleNode pre() { return this.pre; } } //Demo public class TestDoubleNode { public static void main(String[] args) { //创建节点 DoubleNode doubleNode = new DoubleNode(1); DoubleNode doubleNode2 = new DoubleNode(2); DoubleNode doubleNode3 = new DoubleNode(3); //追加节点 doubleNode.after(doubleNode2); doubleNode2.after(doubleNode3); System.out.println(doubleNode.next().getData()); System.out.println(doubleNode.getData()); System.out.println(doubleNode.pre().getData()); System.out.println(doubleNode2.next().getData()); } }
递归:在方法(函数)的内部调用该方法(函数本身的编程方式)
//递归案例
public class TestRecursive {
public static void main(String[] args) {
print(5);
}
public static void print(int n) {
if(n > 0) {
System.out.println(n);
print(n - 1);
}
}
}
//斐波拉契数列案例 public class TestFebonacci { public static void main(String[] args) { //斐波拉契数列:1,1,2,3,5,8,13,21 int i = febonacci(7); System.out.println(i); } private static int febonacci(int i) { if(i == 1 || i == 2) { return 1; }else { return febonacci(i - 1) + febonacci(i - 2); } } }
相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有
三根杆
(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如图1)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。(A,B,C依次为左中右)
public class TestHanoi { public static void main(String[] args) { hanoi(3,'A','B','C'); } /** * * @param i 拥有n个盘子 * @param left 开始的盘子 * @param mid 中间的柱子 * @param right 目标柱子 * 无论有多少盘子都认为只有两个,最下面一个和上面整体 */ private static void hanoi(int i, char left, char mid, char right) { //只有一个盘子 if(i == 1) { System.out.println("第" + i + "个盘子从" + left + "移到" + right); //无论有多少盘子都认为只有两个,最下面一个和上面整体 }else { //移动上面所有盘子到中间位置 hanoi(i - 1, left, right, mid); //移动最下面盘子到目标盘子 System.out.println("第" + i + "个盘子从" + left + "移到" + right); hanoi(i - 1, mid, left, right); } } }
时间复杂度:一般情况下,算法的基本操作语句的重复执行次数就是问题规模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) = n^2 + 5n + 6于T(n) = 3n^2 + 3n + 2 它们的T(n)不同,但时间复杂度相同,都为O(n^2)。
冒泡排序:冒泡排序是一种简单的排序算法,也是笔试题中出现最多的一种排序算法题。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。(‘排序动画’)
算法描述:
- 从一个元素开始,两两比较,如果左边元素比右边大,就交换他们的位置。
- 对每一个相邻元素做相同的工作,这样第一轮比较之后最大的元素就会在最右边。
- 下一轮做重复的操作,但是不用比较最后一个元素。
- 重复以上操作,知道排序完成。
//冒泡排序 public class BubbleSort { public static void main(String[] args) { int[] arr = new int[] {2,9,7,5,1,8,4,7,4,9}; System.out.println(Arrays.toString(arr)); BubbleSort(arr); System.out.println(Arrays.toString(arr)); } private static void BubbleSort(int[] arr) { //比较length - 1轮 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]) { //交换位置 arr[j] = arr[j] ^ arr[j + 1]; arr[j + 1] = arr[j] ^ arr[j + 1]; arr[j] = arr[j] ^ arr[j + 1]; } } } } }
快速排序:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。(‘排序动画’)
算法描述:
- 设置数组第一个数为标准数,设置一个开始下标start和结束下标end
- 拿结束下标指向的最后一个元素和标准数相比,如果比标准数大,下标就减一end–,指向倒数第二个元素,如果比标准数小,就用end指向的元素,替换掉start指向的元素
- 拿开始下标指向的第一个元素和标准数相比,如果比标准数小,下标就加一start++,指向第二个元素,如果比标准数大,就用start指向的元素,替换掉end指向的元素
- 重复23,开始下标和结束下标都重合,把标准数赋值到重合的位置,这样就得到了前部分的所有数比标准数小,后一部分的所有数比标准数大
- 然后分别将前后的作为整体,再进行1234步骤,一次循环递归就会得到顺序数组
- 注意:如果只有以上步骤没有条件判断就会死循环,所有需要加一个判断即,当开始下标小于结束下标时,才能比较排序
//快速排序 public class QuickSort { public static void main(String[] args) { int[] arr = new int[] {3,6,9,8,4,7,1,0,7,5,9,4}; quickSort(arr,0,arr.length - 1); System.out.println(Arrays.toString(arr)); } private 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 && arr[high] >= pivot) { high--; } //如果右边的数字比标准数的小,就把左边的数字替换成右边的数字 arr[low] = arr[high]; //如果左边的数字比标准数小 while(low < high && arr[low] <= pivot) { low++; } //如果左边的数字比标准数大,就把右边的数字替换成左边的数字 arr[high] = arr[low]; } //最后低位和高位重合,一轮循环结束,把标准数赋个重合的这个位置数 arr[low] = pivot; //处理所有比标准数小的数字 quickSort(arr, start, low); //处理所有比标准数大的数字 quickSort(arr, low + 1, end); } } }
插入排序:插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。(‘排序动画’)
算法描述:
- 从一个元素开始遍历(从前往后),如果当前元素比前一个元素大就继续遍历
- 如果当前元素比前一个元素小,记录当前元素
- 遍历当前元素前的所有元素(从后往前),如果当前元素比前一个元素小,那么前一个元素往后移一位,继续往前比较知道当前元素比前一个元素大,就把记录的元素放在当前位置
- 知道第一步遍历结束,排序就完成了
//插入排序 public class InsetSort { public static void main(String[] args) { int[] arr = new int[] {2,7,9,4,7,3,8,1,0,5,8}; insertSort(arr); System.out.println(Arrays.toString(arr)); } private 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 && temp < arr[j]; j--) { //把前一个数字赋给后一个数字 arr[j + 1] = arr[j]; } //把临时变量(外层for循环的当前元素)赋给不满足条件的后一个元素 arr[j + 1] = temp; } } } }
前言:假设有一个数组,数组元素为[3,4,5,6,7,2],如果这个数组用插入排序的话,排序的效率比较低。(‘排序动画’)
希尔排序:1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
算法描述:
- 将元素组的长度除以2得到步长d,根据步长将数组分组(例如:第一个元素是第一个arr[1],那么和第一个元素同组的第二个元素就是arr[1 + d],依次分组)
- 分组的组内元素进行插入排序,第一次分组结束
- 第二次分组再将步长除以2,再将组内的元素进行插入排序
- 重复123知道步长为0,在进行插入排序后,排序完成
//希尔排序 public class ShellSort { public static void main(String[] args) { int[] arr = new int[] {1,7,9,3,7,9,0,3,5,7,2}; shellSort(arr); System.out.println(Arrays.toString(arr)); } private static void shellSort(int[] arr) { int k = 1; //遍历所有步长 for (int d = arr.length / 2; d > 0; d /= 2) { //遍历所有元素 for (int i = d; i < arr.length; i++) { //遍历本组所有元素 for (int j = i - d; j >= 0; j -= d) { //如果当前元素大于加上步长后的那个元素 if(arr[j] > arr[j + d]) { arr[j] = arr[j] ^ arr[j + d]; arr[j + d] = arr[j] ^ arr[j + d]; arr[j] = arr[j] ^ arr[j + d]; } } } System.out.println("第" + k + "次排序后的结果:" + Arrays.toString(arr)); k++; } } }
选择排序:选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
算法描述:
- 先假设第一个为最小,依次遍历比较,如果比标记的数小就交换位置
- 重复第一步知道排序结束
//选择排序 public class SelectSort { public static void main(String[] args) { int[] arr = new int[] {1,6,8,4,5,8,2,9,3}; selectSort(arr); System.out.println(Arrays.toString(arr)); } 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[j] < arr[minIndex]) { minIndex = j; } } //如果最小的数和当前遍历数的下标不一致,说明下表为minIndex的数比当前遍历的数更小 if(i != minIndex) { arr[i] = arr[i] ^ arr[minIndex]; arr[minIndex] = arr[i] ^ arr[minIndex]; arr[i] = arr[i] ^ arr[minIndex]; } } } }
归并排序:归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法描述:
- 把长度为n的输入序列分为两个长度为n/2的子序列;
- 对这两个子序列采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
public class MertgeSort { public static void main(String[] args) { int[] arr = new int[] {1,3,5,2,4,6,8}; System.out.println(Arrays.toString(arr)); mergeSort(arr, 0, arr.length - 1); System.out.println(Arrays.toString(arr)); } //归并排序 public static void mergeSort(int[] arr,int low ,int high) { int middle = (low + high) / 2; if (low < high) { //处理左边 mergeSort(arr, low, middle); //处理右边 mergeSort(arr, middle + 1, high); //归并 merge(arr, low, middle, high); } } public static void merge(int[] arr,int low,int middle,int high) { //用于存储归并后的临时数组 int[] temp = new int[high - low + 1]; //记录第一个数组中需要遍历的下标 int i = low; //记录第二个数组中需要遍历的下标 int j = middle + 1; //用于记录在临时数组中存放的下标 int index = 0; //遍历两个数字取出小的数字,放在临时数组中 while(i <= middle && j <= high) { //第一个数组的数据更小 if(arr[i] < arr[j]) { //把小的数字放入临时数组中 temp[index] = arr[i]; i++; index++; }else { temp[index] = arr[j]; j++; index++; } } //处理多余的数据 while(j <= high) { temp[index] = arr[j]; j++; index++; } while(i <= middle) { temp[index] = arr[i]; i++; index++; } //把临时数组中的值传到原数组 for (int k = 0; k < temp.length; k++) { arr[k + low] = temp[k]; } } }
基数排序:基数排序是按照低位(个位)先排序,然后收集;再按照高位(十位百位等取决于最大数)排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
算法描述:
- 取得数组中的最大数,并取得位数;
- arr为原始数组,从最低位开始每个位组成radix数组;
- 对radix进行计数排序(利用计数排序适用于小范围数的特点)。
public class RadixSort { public static void main(String[] args) { int[] arr = new int[] {23,6,189,45,9,287,56,1,789,34,65,652,5}; radixSort(arr); System.out.println(Arrays.toString(arr)); } public static void radixSort(int[] arr) { //存数组中最大的数字 int max = Integer.MAX_VALUE; for (int i = 0; i < arr.length; i++) { if (arr[i] > max) { max = arr[i]; } } //计算最大数字是几位数 int length = (max + "").length(); //用于临时存储数据的数组 int[][] temp = new int[10][arr.length]; int[] counts = new int[arr.length]; //根据最大长度的数决定比较的次数 for (int i = 0,n = 1; i < length; 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++) { //记录数量的数组中当前余数记录的数量不为0 if(counts[k] != 0) { //循环取出元素 for (int l = 0; l < counts[k]; l++) { //取出元素 arr[index] = temp[k][l]; //记录下一个位置 index++; } //把数量设置为0 counts[k] = 0; } } } } }
队列实现基数排序 --》把二维数组换成队列实现即可
树是一种重要的非线性数据结构,直观地看,它是数据元素(在树中称为结点)按分支关系组织起来的结构,很象自然界中的树那样。
树结构在客观世界中广泛存在,如人类社会的族谱和各种社会组织机构都可用树形象表示。树在计算机领域中也得到广泛应用,如在编译源程序如下时,可用树表示源源程序如下的语法结构。又如在数据库系统中,树型结构也是信息的重要组织形式之一。一切具有层次关系的问题都可用树来描述。
二叉树(Binary tree)是每个节点最多只有两个分支(即不存在分支度大于 2 的节点)的树结构。
二叉树 的分支通常被称作“左子树”或“右子树”。并且,二叉树 的分支具有左右次序,不能随意颠倒。
满二叉树:所有叶子节点都在最后有i曾,而且节点的总数为:2^n - 1 (n是树的高度)。
完全二叉树:所有叶子节点都在最后一层或倒数第二层,且最后一层的叶子节点在
左边连续
,倒数第二节的叶子节点在右边连续
。
二叉树的创建
- 创建节点对象,二叉树对象
- 创建测试类,在测试类中创建二叉树
//节点 public class TreeNode { //节点的权 int value; //左儿子 TreeNode leftNode; //右儿子 TreeNode rightNode; public TreeNode(int value) { this.value = value; } //设置左节点 public void setLeftNode(TreeNode leftNode) { this.leftNode = leftNode; } //设置右节点 public void setRightNode(TreeNode rightNode) { this.rightNode = rightNode; } } //二叉树 public class BinaryTree { TreeNode root; //设置根节点 public void setRoot(TreeNode root) { this.root = root; } public TreeNode getRoot() { return root; } } //Demo public class TestBinaryTree { public static void main(String[] args) { //创建一棵树 BinaryTree binaryTree = new BinaryTree(); //创建一个根节点 TreeNode root = new TreeNode(1); //把根节点赋给树 binaryTree.setRoot(root); //创建一个左节点 TreeNode rootL = new TreeNode(2); //把新创建的节点设置为根节点的子节点 root.setLeftNode(rootL); //创建一个右节点 TreeNode rootR = new TreeNode(3); //把新创建的节点设置为根节点的子节点 root.setRightNode(rootR); } }
遍历二叉树有三种顺序,前序遍历,中序遍历,后续遍历
- 前,中,后的意思是针对根节点在遍历结果中的位置
- 前序遍历就是
先根节点
再是左节点右节点
- 中序遍历就是
先左节点
然后根节点
最后右节点
- 后序遍历就是
先左节点右节点
然后根节点
,总结就是递归的思路实现
//前序遍历 public void frontShow() { //先遍历当前(根)节点的内容 System.out.println(value); //左节点 if(leftNode != null) { leftNode.frontShow(); } //右节点 if (rightNode != null) { rightNode.frontShow(); } } //中序遍历 public void midShow() { //先遍历左节点 if(leftNode != null) { leftNode.frontShow(); } //遍历当前(根)节点的内容 System.out.println(value); //右节点 if (rightNode != null) { rightNode.frontShow(); } } //后序遍历 public void afterShow() { //先左节点 if(leftNode != null) { leftNode.frontShow(); } //然后右节点 if (rightNode != null) { rightNode.frontShow(); } //最后遍历当前(根)节点的内容 System.out.println(value); }
二叉树的查找和遍历很类似,也分为前序查找,中序查找,后续查找三种,下面会写其中的一种
//前序查找 public TreeNode frontSearch(int i) { TreeNode target= null; //对比当前节点的值 if(this.value == i) { return this; //当前节点的值不是要查找的节点 }else { //查找左儿子 if(leftNode != null) { //有可能查到,也可能查不到,查不到的话,target还是一个null target = leftNode.frontSearch(i); } //如果不为空,说明在左儿子已经找到了 if(target != null) { return target; } //左儿子没找到,就在右儿子找 if (rightNode != null) { target = rightNode.frontSearch(i); } } //最后找到就返回,没找就就直接返回null return target; }
删除节点首先要找到需要删除的节点,让这个节点的父节点不指向这个节点(指向null),就等于删除了这个节点
//删除一个子树 public void delete(int i) { TreeNode parentNode = this; //判断左儿子 if(parentNode.leftNode != null && parentNode.leftNode.value == i) { parentNode.leftNode = null; return; } //判断右儿子 if(parentNode.rightNode != null && parentNode.rightNode.value == i) { parentNode.rightNode = null; return; } //递归检查并删除左儿子 parentNode = leftNode; if (parentNode != null) { parentNode.delete(i); } //递归检查并删除左儿子 parentNode = rightNode; if (parentNode != null) { parentNode.delete(i); } }
顺序存储的二叉树:顺序存储的二叉树通常情况下只考虑
完全二叉树
- 第n个元素的左子节点是:2*n + 1
- 第n个元素的右子节点是:2*n + 2
- 第n个 元素的父节点是:(n - 1) / 2
顺序存储的二叉树的遍历,三种,前序,中序,后序遍历
public class ArrayBinaryTree { int[] data; public ArrayBinaryTree(int[] data) { this.data = data; } public void frontShow() { frontShow(0); } //前序遍历 public void frontShow(int index) { if (data == null || data.length == 0) { return; } //先遍历当前节点的内容 System.out.println(data[index]); //处理左子树 2*n + 1 if (2*index + 1 < data.length) { frontShow(2*index + 1); } //处理右子树 2*n + 2 if (2*index + 1 < data.length) { frontShow(2*index + 2); } } } //Demo public class testArrayBinary { public static void main(String[] args) { int[] data = new int[] {1,2,3,4,5,6,7}; ArrayBinaryTree tree = new ArrayBinaryTree(data); //前序遍历 tree.frontShow(); } }
大顶堆:父节点都大于子节点
小顶堆:父节点都小于子节点
升序排序用大顶堆,降序排序用小顶堆
public class HeapSort { public static void main(String[] args) { int[] arr = new int[] {9,6,8,7,0,1,10,4,2}; heapSort(arr); System.out.println(Arrays.toString(arr)); } public static void heapSort(int[] arr) { //开始位置是最后一个非叶子节点,即最后一个节点的父节点 int start = (arr.length - 1)/2; //调整为大顶堆 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); } } }
对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域,利用这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针,这些指针称为线索,加上线索的二叉树称为线索二叉树。
这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树(Threaded BinaryTree)。根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。
**注意:**线索链表解决了无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题,解决了二叉链表找左、右孩子困难的问题。
线索化二叉树时,一个节点的前一个结点,叫前驱节点
线索化二叉树时,一个节点的后一个节点,叫后继节点
赫夫曼树(最优二叉树):它是n个带权节点构成的所有二叉树中,带权路径长度最小的二叉树。
叶节点的带权路径:从根结点出发,经过
节点的数量
乘叶子节点的权值
。树的带权路径长度WPL(weighted path length):树中所有叶子节点的带权路径长度之和。
给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。
创建赫夫曼树的流程分析
排序,取出根节点权值最小的两颗二叉树
组成一颗新的二叉树,前面取出来的两颗二叉树是新二叉的两个子树
根节点的权值是前两取出来的两颗二叉树的根节点的权值之和
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q0yYo3tg-1649250090855)(file:///D:\all app\QQ\qq文件\1273236816\Image\C2C\R~Z4P(S)]PX(RMJOU25YNSC2.png)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d47PdeRS-1649250090857)(file:///D:\all app\QQ\qq文件\1273236816\Image\C2C\LOISR}I%TQ$[`I16SL%C5{V.png)]
//节点对象 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 TestHuffmantree { public static void main(String[] args) { int[] arr = {3,7,8,29,5,11,23,14}; Node node = creatHuffmanTree(arr); System.out.println(node); } //创建赫夫曼树 private static Node creatHuffmanTree(int[] arr) { //先使用数组中所有的元素创建若干个二叉树,(只有一个节点) List<Node> nodes = new ArrayList<Node>(); 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); } }
‘这节建议看原视频’
进行赫夫曼编码
- 先统计每一个byte出现的次数,并放入集合
- 创建一棵赫夫曼树
- 创建赫夫曼编码
- 编码
//赫夫曼树类 public class Node implements Comparable<Node>{ Byte data; int weight; Node left; Node right; public Node(Byte data,int weight) { this.data = data; this.weight = weight; } @Override public int compareTo(Node o) { // TODO Auto-generated method stub //倒叙排序 return o.weight - this.weight; } @Override public String toString() { return "Node [data=" + data + ", weight=" + weight + "]"; } //主函数 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); } /** * 进行赫夫曼编码压缩的方法 * @param bytes * @return */ private static byte[] huffmanZip(byte[] bytes) { //先统计每一个byte出现的次数,并放入集合 List<Node> nodes = getNode(bytes); //创建一棵赫夫曼树 Node tree = creatHuffmanTree(nodes); System.out.println(tree); System.out.println(tree.left); System.out.println(tree.right); //创建一个赫夫曼表 //编码 return null; } /** * 创建赫夫曼树 * @param nodes * @return */ private static Node creatHuffmanTree(List<Node> nodes) { 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(null, left.weight + right.weight); //把之前取出的两棵二叉树设置为新创建的二叉树的子树 parent.left = left; parent.right = right; //把之前取出的两颗二叉树删除 nodes.remove(left); nodes.remove(right); //把新创建的二叉树放入到集合 nodes.add(parent); } return nodes.get(0); } /** * 把数组转换成node集合 * @param bytes * @return */ private static List<Node> getNode(byte[] bytes) { List<Node> nodes = new ArrayList<Node>(); //存储每一个字符出现的次数 Map<Byte, Integer> counts = new HashMap<Byte, Integer>(); //统计每一个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; } }
进行赫夫曼编码
- 先统计每一个byte出现的次数,并放入集合
- 创建一棵赫夫曼树
- 创建赫夫曼编码
- 编码
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); } /** * 进行赫夫曼编码压缩的方法 * @param bytes * @return */ private static byte[] huffmanZip(byte[] bytes) { //先统计每一个byte出现的次数,并放入集合 List<Node> nodes = getNode(bytes); //创建一棵赫夫曼树 Node tree = creatHuffmanTree(nodes); //System.out.println(tree); //System.out.println(tree.left); //System.out.println(tree.right); //创建一个赫夫曼表 Map<Byte, String> guffCodes = getCodes(tree); //System.out.println(guffCodes); //编码 byte[] b = zip(bytes,huffCodes); return b; } /** * 进行赫夫曼编码 * @param bytes * @param huffCodes2 * @return */ private static byte[] zip(byte[] bytes, Map<Byte, String> huffCodes2) { StringBuilder sb = new StringBuilder(); //把需要压缩的byte数组处理成一个二进制的字符串 for (byte b : bytes) { sb.append(huffCodes.get(b)); } //定义长度 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); } //把二进制转换为十进制 byte byt = (byte)Integer.parseInt(strByte,2); //System.out.println(strByte+":"+byt); by[index] = byt; index++; } return by; } /** * 创建赫夫曼编码表 * @param tree * @return */ //用于临时存储路径 static StringBuilder sb = new StringBuilder(); //用于存储赫夫曼编码 static Map<Byte, String> huffCodes = new HashMap<Byte, String>(); 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数组转为二进制的字符串
- 把字符串按照指定的赫夫曼编码进行解码
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); //使用赫夫曼编码进行解码 byte[] newBytes = decoder(huffCodes,b); System.out.println(new String(newBytes)); } /** * 使用指定赫夫曼编码进行解码 * @param huffCodes2 * @param b * @return */ private static byte[] decoder(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()); } //创建list集合,用于存放byte List<Byte> list = new ArrayList<Byte>(); //处理字符串 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; } //把集合转换成数组 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; } }
理解了以上的赫夫曼编码解码写一个压缩文件还是比较容易的
public class TestHuffmanCode { public static void main(String[] args) { String src = "C:\\Users\\zhou\\Desktop\\1.txt"; String dst = "C:\\Users\\zhou\\Desktop\\2.zip"; try { zipFile(src,dst); } catch (IOException e) { e.printStackTrace(); } } /** * 压缩文件 * @param src * @param dst * @throws IOException */ private static void zipFile(String src, String dst) throws IOException{ //创建一个输入流 InputStream input = new FileInputStream(src); //创建一个和输入流流向的文件大小一样的byte数组 byte[] b = new byte[input.available()]; //读取文件内容 input.read(b); input.close(); //使用赫夫曼编码进行编码 byte[] byteZip = huffmanZip(b); //输出流 OutputStream os = new FileOutputStream(dst); ObjectOutputStream oos = new ObjectOutputStream(os); //把压缩后的byte数组写入文件夹 oos.writeObject(byteZip); //把赫夫曼编码表写入文件 oos.writeObject(huffCodes); oos.close(); os.close(); }
public class TestHuffmanCode { public static void main(String[] args) { String src = "C:\\Users\\zhou\\Desktop\\2.zip"; String dst = "C:\\Users\\zhou\\Desktop\\3.txt"; try { unZip(src, dst); } catch (Exception e) { e.printStackTrace(); } } /** * 解压文件 * @param src * @param dst * @throws Exception */ public static void unZip(String src,String dst) throws Exception{ //创建输入流 InputStream input = new FileInputStream(src); ObjectInputStream ois = new ObjectInputStream(input); //读取byte数组 byte[] b = (byte[])ois.readObject(); //读取赫夫曼编码表 Map<Byte, String> codes = (Map<Byte, String>)ois.readObject(); ois.close(); input.close(); //解码 byte[] bytes = decoder(codes, b); //创建一个输出流 OutputStream output = new FileOutputStream(dst); //写出数据 output.write(bytes); output.flush(); output.close(); }
二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。定义:对于二叉树中的任何一个非叶子节点,要求左子节点比当前节点小,右子节点比当前节点大。一棵空树也算二叉排序树。
创建二叉排序树&添加节点
//节点类 public class Node { int value; Node left; Node right; public Node(int value) { this.value = value; } /** * 添加节点 * @param 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); } } } /** * 中序遍历 * @param root */ public void frontShow(Node node) { if(node == null) { return; } frontShow(node.left); System.out.println(node.value); frontShow(node.right); } } //二叉树类 public class BinarySortTree { Node root; public void add(Node node) { //如果是一颗空树 if(root == null) { root = node; }else { root.add(node); } } /** * 中序遍历二叉树,从小到大排序 */ public void frontShow() { if(root!=null) { root.frontShow(root); } } } //测试方法 public class TestBinarySortTree { public static void main(String[] args) { int[] arr = new int[] {7,3,10,12,5,1,9}; //创建一棵二叉排序树 BinarySortTree tree = new BinarySortTree(); //循环添加 for (int i : arr) { tree.add(new Node(i)); } //查看树中的值 tree.frontShow(); } }
二叉排序树中查找结点
//二叉树类 /** * 节点的查找 * @param value * @return */ public Node search(int value) { if (root == null) { return null; }else { return root.search(value); } } //节点类 public Node search(int value2) { if (this.value == value2) { return this; }else if(this.value > value2) { if (left == null) { return null; } return left.search(value2); }else { if (right == null) { return null; } return right.search(value2); } }
删除节点有三种情况:
- 删除的节点为根节点
- 删除的节点是有一棵子树的节点
- 删除的节点是有两颗子树的节点
//排序树类中的方法 /** * 删除节点 * @param value */ public void delete(int value) { if(root == null) { return; }else { //找到这个节点 Node node = search(value); //如果没有这个节点 if (node == null) { return; } //找到他的父节点 Node searchParent = searchParent(value); //1.要删除的节点是叶子节点 if (node.left == null && node.right == null) { //要删除的是父节点的左子节点 if(searchParent.left.value == value) { searchParent.left = null; } //要删除的是父节点的右子节点 if(searchParent.right.value == value) { searchParent.right = null; } //2.要删除的节点有两个子节点的情况 }else if (node.left != null && node.right != null) { //删除右子树中值最小的节点,获取到该节点的值 int min = deleteMin(node.right); //替换目标节点中的值 node.value = min; //3.要删除的节点只有一个左子节点或右子节点 }else { //有左子节点 if (node.left != null) { //要删除的节点是父节点的左子节点 if (searchParent.left.value == value) { searchParent.left = node.left; //要删除的节点是父节点的右子节点 }else { searchParent.right = node.left; } //有右子节点 }else{ //要删除的节点是父节点的左子节点 if (searchParent.left.value == value) { searchParent.left = node.right; //要删除的节点是父节点的右子节点 }else { searchParent.right = node.right; } } } } } /** * 删除一棵树中最小的节点 * @param right * @return */ private int deleteMin(Node right) { Node target = right; //递归向左找 while(target.left != null) { target = target.left; } //删除最小的节点 delete(target.value); return target.value; } /** * 搜索父节点 * @param value * @return */ public Node searchParent(int value) { if (root == null) { return null; }else { return root.searchParent(value); } } } //节点类中的方法 /** * 搜索父节点 * @param value2 * @return */ public Node searchParent(int value2) { if ((this.left!=null&&this.left.value==value2) || (this.right!=null&&this.right.value==value2)) { return this; }else if(this.value > value2 && this.left != null) { return this.left.searchParent(value2); }else if(this.value < value2 && this.right != null) { return this.right.searchParent(value2); } return null; } //主方法测试 public class TestBinarySortTree { public static void main(String[] args) { int[] arr = new int[] {7,3,10,12,5,1,9}; //创建一棵二叉排序树 BinarySortTree tree = new BinarySortTree(); //循环添加 for (int i : arr) { tree.add(new Node(i)); } //查看树中的值 //tree.frontShow(); // Node search = tree.search(10); // System.out.println(search.value); // Node search2 = tree.search(20); // System.out.println(search2); //测试查找父节点 // Node searchParent = tree.searchParent(1); // System.out.println(searchParent.value); //删除叶子节点 // tree.delete(12); // tree.frontShow(); System.out.println("==="); //删除只有一个子节点的节点 // tree.delete(10); // tree.frontShow(); //删除有两个子节点的节点 tree.delete(3); tree.frontShow(); } }
平衡二叉树,又称AVL树。它或者是一棵
空树
,或者是具有下列性质的二叉树:对于任何一个子树而言,它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度之差之差的绝对值不超过1.。常用算法有:红黑树、AVL树、Treap等。
/** * 返回当前节点的高度 * @return */ public int height() { return Math.max(left==null?0:left.height(), right==null?0:right.height()) + 1; } /** * 获取左子树的高度 * @return */ public int leftHeight() { if(left == null) { return 0; } return left.height(); } /** * 获取右子树的高度 * @return */ public int rightHeight() { if(right == null) { return 0; } return right.height(); } /** * 添加节点 * @param 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); } } //查询是否平衡 //进行右旋转 if (leftHeight() - rightHeight() >= 2) { rightRotate(); } //左旋转 if (leftHeight() - rightHeight() <= -2) { leftRotate(); } } /** * 左旋转(与右旋转全部相反即可) */ private void leftRotate() { Node newLeft = new Node(value); newLeft.left = left; newLeft.right = right.left; value = right.value; right = right.right; left = newLeft; } /** * 右旋转 */ 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 != null&&left.leftHeight()<right.rightHeight()) { //先左旋转 left.leftRotate(); //再右旋转 rightRotate(); //单旋转 }else { rightRotate(); } } //左旋转 if (leftHeight() - rightHeight() <= -2) { //双旋转 if(right != null&&right.leftHeight()<right.rightHeight()) { //先右旋转 right.rightRotate(); //再左旋转 leftRotate(); //单旋转 }else { leftRotate(); } }
数据存储的方式:硬盘和内存。
内存:
- 优点:使用电信号来保存数据的,不存在机器操作,所以访问速度非常快
- 缺点:造价高,断电后数据丢失。一般作为CPU的高速缓存
磁盘:
- 优点:造价低,容量大,断电数据不丢失
- 缺点:由于存储介质的特性,再加上机械运动耗费时间,所以磁盘的速度比较慢。
磁盘的预读:
由于磁盘的读写速度问题,要尽量减少磁盘I/0操作。所以磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。这样做的理论依据是计算机科学中著名的局部性原理:
当一个数据被用到时,其附近的数据也通常会马上被使用。
预读的长度一般为页(page) 的整倍数.
页:
页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为(4k),主存和磁盘以页为单位交换数据。
文件系统及数据库系统的设计者利用了磁盘预读原理,将-一个节点的大小设为等于- 一个页,这样每个节点只需要一次 I/0就可以完全载入。
2-3树是最简单的B-树(或-树)结构,其每个非叶节点都有两个或三个子女,而且所有叶都在统一层上。2-3树不是二叉树,其节点可拥有3个孩子。不过,2-3树与满二叉树相似。若某棵2-3树不包含3-节点,则看上去像满二叉树,其所有内部节点都可有两个孩子,所有的叶子都在同一级别。另一方面,2-3树的一个内部节点确实有3个孩子,故比相同高度的满二叉树的节点更多。高为h的2-3树包含的节点数大于等于高度为h的满二叉树的节点数,即至少有2^h-1个节点。换一个角度分析,包含n的节点的2-3树的高度不大于log2(n+1)。
B树也是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例,我们把树中结点最大的孩子数目称为B树的阶。通常记为m。
一棵m阶B树或为空树,或为满足如下特性的m叉树:
- 树中每个结点至多有m棵子树。(即至多含有m-1个关键字) (“两棵子树指针夹着一个关键字”)
- 若根结点不是终端结点,则至少有两棵子树。(至少一个关键字)
- 除根结点外的所有非叶结点至少有 ⌈m/2⌉棵子树。(即至少含有⌈m/2⌉-1个关键字)
- 所有非叶结点的结构如下:
- 所有的叶子结点出现在同一层次上,不带信息。(就像是折半查找判断树中查找失败的结点)
B+树是应文件系统所需而产生的一种B-tree的变形树。
一棵m阶的B+树和m阶的B树的异同点在于:
- 有n棵子树的结点中含有n-1 个关键字;
- 所有的叶子结点中包含了全部关键字的信息,及指向含有这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大的顺序链接。
- 所有的非终端结点可以看成是
索引
部分,结点中仅含有其子树根结点中最大(或最小)关键字。 (而B 树的非终节点也包含需要查找的有效信息)
B+的特性:
1).所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;
2).不可能在非叶子结点命中;
3).非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;
4).更适合
文件索引系统
;B+树的优势:
1.单一节点存储更多的元素,使得查询的IO次数更少。
2.所有查询都要查找到叶子节点,查询性能稳定。
3.所有叶子节点形成有序链表,便于范围查询
磁盘中B+树索引:
局部性原理与磁盘预读,预读的长度一般为页(page)的整倍数,(在许多操作系统中,页得大小通常为4k)
数据库系统巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入,(由于节点中有两个数组,所以地址连续)。
散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。
散列函数的设计
- 直接定址法
- 数字分析法
- 平方取中法
- 取余法
- 随机数法
哈希表的底层结构就是一个
数组
,数组的长度即哈希表的长度,数组中的每个空间
(也叫桶)存放的是一条链表
,链表中的每个节点用来存放元素。**即一个数组的每个数组元素是一条链表,链表的每个节点存放元素,可以将数组的每个元素看做桶,桶里面可以有多个元素。**桶里元素直接的数据结构是链表。
开放地址法
- 线性探测法
- 二次探测法
- 再哈希法
链地址法
顶点
图中的数据元素,我们称之为顶点,图至少有一个顶点(非空有穷集合)
对应到好友关系图,每一个用户就代表一个顶点。
边
顶点之间的关系用边表示。
对应到好友关系图,两个用户是好友的话,那两者之间就存在一条边。
度
度表示一个顶点包含多少条边,在有向图中,还分为出度和入度,出度表示从该顶点出去的边的条数,入度表示进入该顶点的边的条数。
对应到好友关系图,度就代表了某个人的好友数量。
无向图和有向图
边表示的是顶点之间的关系,有的关系是双向的,比如同学关系,A是B的同学,那么B也肯定是A的同学,那么在表示A和B的关系时,就不用关注方向,用不带箭头的边表示,这样的图就是无向图。
有的关系是有方向的,比如父子关系,师生关系,微博的关注关系,A是B的爸爸,但B肯定不是A的爸爸,A关注B,B不一定关注A。在这种情况下,我们就用带箭头的边表示二者的关系,这样的图就是有向图。
无权图和带权图
对于一个关系,如果我们只关心关系的有无,而不关心关系有多强,那么就可以用无权图表示二者的关系。
对于一个关系,如果我们既关心关系的有无,也关心关系的强度,比如描述地图上两个城市的关系,需要用到距离,那么就用带权图来表示,带权图中的每一条边一个数值表示权值,代表关系的强度。
图代码实现
- 顶点用数组表示
- 邻接矩阵用二维数组
/** * 顶点类 */ public class Vertex { private String value; public String getValue() { return value; } public void setValue(String value) { this.value = value; } public Vertex() { super(); } public Vertex(String value) { super(); this.value = value; } @Override public String toString() { return value ; } } /** * 图结构 */ 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]; } /** * 向途中加入一个顶点 * @param v */ 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 class TestGraph { 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)); } } }
深度优先搜索算法(DFS):
- 我们假设初始状态所有顶点都没被访问,然后从每一顶点v出发,先访问该顶点
- 然后依次从他的各个未访问的邻接点出发,深度优先遍历图,直到图中所有和v相通的顶点都被访问到。
- 遍历完之后,还有其他顶点没有被访问到,则另选一个未被访问的定点作为起始点。
- 重复上述过程,直到所有顶点 都被访问完为止。
广度优先搜索算法(BFS):
- 从图中的某一顶点出发,在访问了v之后依次访问v的各个没有访问到的邻接点
- 然后分别从这些邻接点出发依次访问他们的邻接点,使得先被访问的顶点的邻接点先与后被访问顶点的邻接点被访问,知道图中所有已被访问的顶点的邻接点都被访问到。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。