数据结构与算法是一门相对比较枯燥,但也是编程人员必学的课程。通过学习数据结构与算法,可以提高我们在编程中的思维逻辑;本人也是小白一枚,学习了一段时间后,在这边总结下;尽量以比较白话的方式,来进行更好的理解。
数据结构可以简单的理解为数据与数据之间所存在的一些关系,数据的结构分为数据的存储结构和数据的逻辑结构。
1、顺序存储结构:就是将数据进行连续的存储,我们可以将它比喻成学校食堂打饭排队一样,一个接着一个;
2、链式存储结构:不是按照顺序存储的,后一个进来的数只需要将他的地址告诉前一个节点,前一个节点中就存放了它后面那个数的地址,所以最后一个数的存储地址就是为null;可以将这种结构比喻成商场吃饭叫号,上面的号码比喻成是地址,你可以之后后面的地址是什么,上面的其他内容就是该节点的内容;
两种方式的区别:
顺序存储的特点是查询快,插入或者删除慢;还是用上述的食堂打饭来进行形容,队伍排在那里,所有人都在那,一目了然,很快可以找到目标;但是突然有个人去了面前插队,他插队的话就会导致在他后面的所有人都需要往后移动,就相当于移动了太多的数据了。
链式存储的特点是查询慢,插入或者删除快;我们去商场一家火爆的店,取了号之后,我们可以选择在门口坐等,也可以选择先去逛逛其他地方,这样想去找到某一个人,就不是那么方便了,你需要一个一个去看;这个时候这家店来了位超级vip,前台直接给他安排了前面的号码,其他人的号码都没有变动,只是在某一个号码的后面指向了新插入的这个值,然后将新插入这个号码的地址指向上一个号码要指向的地址就可以了,对其他人都没有影响。
1、集合结构:数据元素同属于一个集合,他们之间是并列关系,无其他的关系;可以理解为中学时期学习的集合,在一个范围之内,有很多的元素,元素间没有什么关系;
2、线性结构:元素之间存在着一对一的关系;可以理解为每个学生对应着一个学号,学号与姓名就是线性结构;
3、树形结构:元素之间存在着一对多的关系,可以简单理解为家庭族谱一样,一代接一代;
4、图形结构:元素之间存在多对多的关系,每一个元素可能对应着多个元素,或被多个元素对应,网状图。
算法可以理解为遇到问题时解决问题的思路,上学时期学习数学的每一种思路,或者我们解决算法题的每种思路都能称为算法。
1、输入:至少有一个入口,就像结题时都需要有条件;
2、输出:至少有一个出口,解题最后的答案;
3、有穷性:在有限的时间,步骤中有执行结果;
4、确定性:与输入相对应的结果是确定的;
5、可行性:必须是可以实际执行的;
1、正确性:要确保这个算法是正确的;
2、可读性:能够让别的程序员能看懂的程序;
3、健壮性:符合某一类的完整性,不能只局限与某一小部分,需要考虑完善,考虑到各种情况的发生;
4、时间复杂度:算法占用的时间,与空间复杂度是相对的;
5、空间复杂度:算法所占用的内存;
算法没有最好的,只有最合适的,学习算法是为了积累学习思路,掌握学习思路,并不是为了解决某问题去记住某种算法;对于时间复杂度与空间复杂度,现在大多数开发情况下,我们都在使用以空间换时间,耗费更多的内存,来保证拥有更快的速度。
数组是典型的顺序存储结构,数组的长度是不可变的,想要对数组进行增删都需要创建新的数组然后重新赋值;面向对象的数组就是封装一些方法用来操作数组;例如:insert,add,get,delete等方法;查找数组中的方法有两种,线性查找和二分法查找;线性查找就是简单的查找数组中的元素;二分法查找要求目标数组必须是有序的。
public class MyArray { //用于存储数据的数组 private int[] elements; public MyArray() { elements = new int[0]; } //获取数组长度的方法 public int size() { return elements.length; } //往数组末尾添加元素 public void add(int element) { int[] newArray = new int[elements.length+1]; for(int i=0;i<elements.length;i++) { newArray[i] = elements[i]; } newArray[elements.length] = element; elements = newArray; } //打印所有元素 public void show() { System.out.print("[" + " "); for(int i=0;i<elements.length;i++) { System.out.print(elements[i]+" "); } System.out.print("]"); } //删除数组中的某个值的所有元素 public void delete(int element) { int a = 0; int[] newArray = new int[elements.length-1]; for(int i=0;i<elements.length;i++) { if(elements[i]==element) { continue; } newArray[a++] = elements[i]; } elements = newArray; } //根据索引找出某个数 public int get(int index) { return elements[index]; } //插入一个元素到指定位置 public void insert(int index,int element) { int num = 0; int[] newArray = new int[elements.length+1]; for(int i=0;i<newArray.length;i++) { if(i==index) { newArray[i++] = element; } newArray[i] = elements[num++]; } elements = newArray; } //替换指定位置元素 public void set(int index,int element) { /* * int[] newArray = new int[elements.length]; for(int i=0;i<newArray.length;i++) * { if(i==index) { newArray[i] = element; continue; } newArray[i] = * elements[i]; } elements = newArray; */ if(index<0||index>elements.length-1) throw new RuntimeException("下标越界"); elements[index] = element; } //线性查找 public int search(int target) { for(int i=0;i<elements.length;i++) { if(elements[i]==target) { return i; } } return -1; } //二分法查找 public int binarySearch(int target) { int begin = 0; //开始位置 int end =elements.length-1; //结束位置 int mid = (begin + end)/2; int index = -1; while(begin<=end) { if(elements[mid]==target) { index = mid; return index; }else { if(elements[mid]>target) { end = mid-1; }else { begin = mid+1; } mid = (begin + end)/2; } } mid = (begin + end)/2; /* * do { if(elements[mid]==target) { index = mid; }else { * if(elements[mid]>target) { end = mid-1; }else { begin = mid+1; } mid = (begin * + end)/2; } }while(begin>=end); */ if(elements[mid]==target) { return index; }else { return -1; } } }可以将栈理解成一个水杯,只有一个出入口,所以是先进后出,先进去的水在最下面,也是最后出来的;向栈中压入元素也是使用的数组,要从最后开始加入(这里与数组中是一致的);向其中取数据就与数组中不一致了,需要从后面往前进行取数据,还要将数组的最后一位的位置去除,才能保证,下一个也是最后一个,要是不讲最后一位的位置去除,下一次取出的就是null值。
public class MyStack { //栈的底层使用数组来存储数据 private 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("数组长度为0"); } //取出数组最后一个元素 int element = elements[elements.length-1]; //取出最后一个元素的位置 int[] arr = new int[elements.length-1]; for(int i=0; i<arr.length; i++) { arr[i] = elements[i]; } elements = arr; return element; } //查看栈顶元素 public int peek() { //当栈中没有元素 if(elements.length==0) { throw new RuntimeException("数组长度为0"); } return elements[elements.length-1]; } //判断栈是否为空 public boolean isEmpty() { return elements.length==0; } }就如同排队,先进先出,从队尾进,从队头出。
public class MyQueue { private int[] elements; public MyQueue() { elements = new int[0]; } //入队 public void add(int element) { int[] newArray = new int[elements.length+1]; for(int i=0;i<elements.length;i++) { newArray[i] = elements[i]; } newArray[elements.length] = element; elements = newArray; } //出队 public int poll() { if(elements.length==0) { throw new RuntimeException("队列长度为0"); } int element = elements[0]; //创建新数组 int[] newArr = new int[elements.length-1]; for(int i=1;i<elements.length; i++) { newArr[i-1] = elements[i]; } elements = newArr; return element; } //判断队列是否为空 public boolean isEmpty() { return elements.length==0; } }与上述三种不一样,上面所写的三种都是数组形式的顺序存储。我们的单链表是以链表的形式,通常用Node表示一个节点,一个节点包含节点内容,还有下一个节点的地址;链表的核心就是指针域,就是前面说的节点地址;通过控制指针域来操作增加,删除,插入节点等。
节点的删除,因为是单链表,所以只能往后面找,只能删除当前节点的下一个节点,将当前节点的指针域指向下下个节点,就完成了对下一个节点的删除。
节点的插入,先找到当前节点的下一个节点,将当前节点的指针域指向新节点,新节点的指针域指向刚才取出的那个节点,就完成了一次节点的插入。
//node表示一个节点 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 void after(Node node) { Node afterNode = this.next; this.next = node; node.next = afterNode; } //显示所有节点信息 public void show() { Node currentNode = this; while(true) { System.out.print(currentNode.data + " "); currentNode = currentNode.next; if(currentNode==null) { break; } } } //删除一个节点 public void removeNext() { Node newNode = this.next.next; this.next = newNode; } //获取下一个节点 public Node next() { return this.next; } //获取节点中的数据 public int getData() { return this.data; } //当前节点是否是最后一个节点 public boolean isLast() { return next == null; } }与单链表的唯一区别就是没有最后一个节点,将单链表中的最后一个节点的指针域指向第一个节点,形成一个闭环的回路,就形成了循环链表。
//node表示一个节点 public class LoopNode { //节点内容 int data; //下一个节点 LoopNode next = this; public LoopNode(int data) { this.data = data; } //插入一个节点 public void after(LoopNode node) { LoopNode afterNode = this.next; this.next = node; node.next = afterNode; } //删除一个节点 public void removeNext() { LoopNode newNode = this.next.next; this.next = newNode; } //获取下一个节点 public LoopNode next() { return this.next; } //获取节点中的数据 public int getData() { return this.data; } }每一个节点都有两块指针域,每一个节点都要指向他的前一个节点,还要后一个节点,这样不论是想要获取前面节点内容还是后面节点内容都是可以的;前面的单链表是不能获得前面节点的值。
双链表的实现思路:循环的双向链表,首先要将上一个节点与下一个节点都指向自己,就是先创建单节点的双向循环,在之后添加新节点的时候再进行打破节点的指向,这样就能保证链表双向循环。
public class DoubleNode { //上一个节点 DoubleNode pre = this; //下一个节点 DoubleNode next = this; //节点的内容 int data; public DoubleNode(int data) { this.data = data; } //增加节点 public void after(DoubleNode node) { DoubleNode nextNext = next; this.next = node; node.pre = this; node.next = nextNext; nextNext.pre = node; } //获取下一个节点 public DoubleNode next() { return this.next; } //获取上一个节点 public DoubleNode pre() { return this.pre; } //获取数据方法 public int getData() { return this.data; } }通过一个调用的语句不断继续调用自己的这个方法,如果一直调用,没有出口的话会造成栈溢出;在之前看到的教学讲解中都是讲调用自己的方法,其实我个人认为这样会比较容易晕,将它理解成去调用相同的方法,只是其中的参数值变化了,理解成一个分身的形式,不断地去调用下一个分身,返回的时候,从最后一个分身往前返回,知道第一个真身跳出,就结束。
汉诺塔游戏是经典的递归游戏:
可以理解成只有两种情况,当其中只有一个盘子,直接从A移动到C;当有两个的时候将第一个移动到B,最下面的移动到C,再将第一个移动到C;所以当有n个的时候,将上面的n-1当做一个整体移动到B,将n移动到C,再将上面的n-1移动到C。
public static void hanoi(int n,char x,char y,char z) { if(n==1) { System.out.println(x+"----------->"+z); }else { //将前面n-1个移到y上 hanoi(n-1,x,z,y); //将最下面那个移到Z上 System.out.println(x+"----------->"+z); //将前面n-1个移到z上 hanoi(n-1, y, x, z); } }冒泡排序
原理:如同水里的泡泡,不断向上,每次把最大的排到上面;每一次找出最大的那个数,将它放到最后,已经被排的数之后就不需要进行排序。需要排序length-1次,每次找出一个数,最后一次只有一个数没有排序,但是那个数一定是最小的,所以不需要再排序。需要两个for循环,第一个for循环用来表示循环的次数,第二个for循环用来比较,并且进行交换元素。
//冒泡排序 public void BubbleSort(int[] array) { int temp = 0; int n=array.length; for(int i=0; i<array.length-1; i++) { //比较多少轮 for(int j=1; j<n;j++) { //数字之间的比较 if(array[j-1]>array[j]) { temp = array[j-1]; array[j-1] = array[j]; array[j] = temp; } } n--; } }快速排序
原理:首先将他们分为大小两部分,左边的都比参考值要小,右边的都比参考值要大。有两个指针,分别先在数组的两端;再找一个参考值,通常以第一个值作为参考值,从最后的指针开始向前移动,当指针上的值比参考值要大,那就继续往前移动,直到指针上的值比参考值小,这时将这个值,直接覆盖掉左侧指针上的值;然后开始移动左侧指针上的值,当指针上的值比参考值小继续向后移动,直到指针上的值比参考值大,直接将该指针上的值,覆盖到右侧指针上的值;又移动右侧的指针;直到左侧指针等于右侧指针的位置,将参考值覆盖该指针上的值。此时,一个数组就被分为两部分,再对这两个数组进行递归,以此,直到排序完成。
//快速排序 public void quickSort(int[] arr,int start,int end) { if(start<end) { //指定标准值 int stard = arr[start]; //需要排序的起始位置 int low = start; //需要排序的最终位置 int high = end; while(low<high) { //当后面指针上的数大于比较值 将指针向前移 while(low<high&&stard<=arr[high]) { high--; } //当指针上的数小于比较值 将后面小的值放到前面来 arr[low] = arr[high]; //当前面指针上的数小于比较值 将指针向后移 while(low<high&&stard>=arr[low]) { low++; } //当指针上的数大于比较值 将该值替换给后面的值 arr[high] = arr[low]; } //当前后指针相同时 将标准值放在该位置 arr[low] = stard; //将0到low进行递归 quickSort(arr,start,low); //将low+1到最后进行递归 quickSort(arr,low+1,end); } }直接插入
与冒泡排序类似,从第二个值开始与前面的比较,前面的值是有序的,当比较到前面的值比他小时,就不再往前比较,当前面的值比他大就进行交换,再进行往前比较;该排序也需要两层循环,第一层循环用于遍历数组,不过是从第二个开始;第二层循环用来往前比较并交换元素。
//插入排序 public void insertSort(int[] arr) { int temp =0; //用来指向要交换的值 int a=0;//将比较后的值往前移动,继续比较 //int count =0; for(int i=1;i<arr.length;i++) { //遍历从1开始的所有元素 a = i; /* * 与前面的元素进行比较,一直往前面比, * 当前面的数字比后面小的时候直接跳出 * 不再往前比较*/ for(int j=i-1;j>=0;j--) { if(arr[a]<arr[j]) { temp = arr[a]; arr[a] = arr[j]; arr[j] = temp; a--; }else { //进行优化 当不需要再进行往前比较时就退出 break; } } } //System.out.println(count); }希尔排序
先将数组长度除2,得到的数就是分成的组数n;将第一个数与他后面的n/2*1,n/2*2.....数进行比较;第二个数与加n个数进行比较(分成这么多组再对其中的每一组进行跟直接插入方式一样的排序);第一轮结束后再进行除2,将组数减少,直接最后只有一组,排序完成。
希尔排序比直接插入排序的优点:当数据量大的时候,如果在数组末尾插入一个很小的数,使用直接插入排序比较的次数太多,效率就比较的低;使用希尔排序,利用步长的特点,可以在第一次就将这个较小的数移到前面,大大减少了交换数据的次数。
//希尔排序 public void shellSort(int[] arr) { /*首先需要先遍历所有的步长 * 其实就是比直接插入多了这层, * 这也是希尔排序的精髓所在*/ for(int d =arr.length/2;d>0;d/=2) { //与插入排序类似从第二个开始,这里就是从d开始 for(int i=d;i<arr.length;i++) { //遍历本组中的所有数据 for(int j=i-d;j>=0;j-=d) { if(arr[j]>arr[j+d]) { int temp = arr[j]; arr[j] = arr[j+d]; arr[j+d] = temp; }else { break; } } } } }简单选择排序
每次对数组进行遍历,找出一个最小的数,与前面没有进行排序的数字进行交换;每遍历一次,前面就多一个数排序完成,排序完成的数不需要参加之后的排序。
//简单选择排序 public void selectSort(int[] arr) { //表示用来循环的次数 for(int i=0; i<arr.length-1; i++) { int temp = i; //记录最小的数 //用来遍历一次找出最小的数 for(int j=i+1; j<arr.length; j++) { if(arr[j]<arr[temp]) { temp = j; } } if(temp!=i) { int num = arr[temp]; arr[temp] = arr[i]; arr[i] = num; } } }堆排序
堆分为大顶堆,小顶堆;大顶堆就是任意一个节点都比他的所有节点都要大;小顶堆于此相反。首先从最后一个非叶子节点进行排序,将树排列成一个大顶堆;然后将根节点与最后一个节点进行交换,取出这时的最后一个节点放在数组的最后,每次取出的都是大顶堆中的最大的数,所以就组成了升序的数组。
/** * 大顶堆 * arr 数组 * size 排序的数量(从length开始递减) * index 从哪个开始比较(最后一个非叶子节点) */ public 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); } } //对上面完成的大顶堆进行堆排序 public void heapSort(int[] arr) { //调整为大顶堆 int start = (arr.length-2)/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); } }
原理:首先需要10个数组0——9;第一次取出每个数的个位数,放在下标相同的数组中,排完后挨个取出;再进行第二轮取出每个数的十位数,同样放在下标相对应的数组中;以此类推,最大的数有几位数就需要进行几轮;从个位数开始,能够保证从低位开始有序。
//基数排序 public 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[] count = 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][count[ys]] = arr[j]; count[ys]++; } //记录取的元素需要放的位置 int index=0; //把数字取出来 for(int k=0;k<count.length; k++) { if(count[k]!=0) { //循环取出元素 for(int l=0; l<count[k]; l++) { //取出元素 arr[index] = temp[k][l]; index++; } //把数量置为0 count[k] = 0; } } } }
树结构可以理解成,一个树根,树根上有多个分支,分支上面还有不定的分支,最后是叶子,叶子就没有了分支。
线性结构中不论是数组还是链表,他们都存在着诟病;比如查找某个数必须从头开始查,消耗较多的时间。使用树结构,在插入和查找的性能上相对都会比数组要好。
1、根节点:最顶上的唯一的一个
2、双亲节点:子节点的父节点就叫做双亲节点
3、子节点:双亲节点所产生的节点就是子节点
4、路径:从根节点到目标节点所走的路程叫做路径
5、节点的度:有多少个子节点就有多少的度(最下面的度一定为0,所以是叶子节点)
6、节点的权:在节点中所存的数字
7、叶子节点:没有子节点的节点,就是没有下一代的节点
8、子树:在整棵树中将一部分看成也是一棵树,即使只有一个节点也是一棵树,不过这个树是在整个大树职中的,包含的关系
9、层:就是族谱中有多少代的人
10、树的高度:树的最大的层数:就是层数中的最大值
11、多个数组成的集合
任何一个节点的子节点数量不超过二,那就是二叉树;二叉树的子节点分为左节点和右节点。
满二叉树就是所有的叶子节点都在最后一层,其他的节点都有两个子节点;完全二叉树就是所有的叶子节点都在最后一层或者倒数第二层,且最后一层叶子节点在左边连续,倒数第二层在右边连续,就是中间不能出现断续的状态;(满二叉树也是属于完全二叉树)。
创建二叉树:首先需要一个树的类,还需要另一个类用来存放节点,设置节点;将节点放入树中,就形成了二叉树;(节点中需要权值,左子树,右子树,并且都能对他们的值进行设置)。
树的遍历:
前序遍历:根节点,左节点,右节点
中序遍历:左节点,根节点,右节点
后序遍历:左节点,右节点,根节点
查找节点:先对树进行一次遍历,然后找出要找的那个数;因为有三种排序方法,所以查找节点也分为前序查找,中序查找,后序查找;
删除节点:注意点:由于链式存储,不能找到要删的数再删除,需要找到他的父节点,然后将指向该数设置为null;所以需要一个变量来指向父节点,找到数后,再断开连接。
public class TreeNode { //节点的权 int value; //左儿子 TreeNode lNode; //右儿子 TreeNode rNode; public TreeNode(int value) { this.value = value; } //设置左节点 public void setlNode(TreeNode lNode) { this.lNode = lNode; } //设置右节点 public void setrNode(TreeNode rNode) { this.rNode = rNode; } //前序遍历 public void frontShow() { //当前节点 System.out.print(value + " "); //左节点 if(lNode!=null) { lNode.frontShow(); } //右节点 if(rNode!=null) { rNode.frontShow(); } } //中序遍历 public void midShow() { //左节点 if(lNode!=null) { lNode.midShow(); } //当前节点 System.out.print(value + " "); //右节点 if(rNode!=null) { rNode.midShow(); } } //后序遍历 public void backShow() { //左节点 if(lNode!=null) { lNode.backShow(); } //右节点 if(rNode!=null) { rNode.backShow(); } //当前节点 System.out.print(value + " "); } //前序查找 public TreeNode frontSearch(int i) { TreeNode target = null; if(this.value == i) { return this; }else { if(lNode!=null) { target = lNode.frontSearch(i); } if(target!= null) { return target; } if(rNode!=null) { target = rNode.frontSearch(i); } } return target; } //删除一颗子树 public void delete(int i) { TreeNode parent = this; //判断左儿子 if(parent.lNode!=null&&parent.lNode.value == i) { parent.lNode = null; return; } //判断右儿子 if(parent.rNode!=null&&parent.rNode.value == i) { parent.rNode = null; return; } //递归左儿子 parent = lNode; if(parent!=null) { parent.delete(i); } //递归右儿子 parent = rNode; if(parent!=null) { parent.delete(i); } } } public class Tree { TreeNode root; //设置根节点 public void setRoot(TreeNode root) { this.root = root; } //获取根节点 public TreeNode getRoot() { return root; } //前序遍历 public void frontShow() { if(root!=null) { root.frontShow(); } } public void midShow() { if(root!=null) { root.midShow(); } } public void backShow() { if(root!=null) { root.backShow(); } } public TreeNode frontSearch(int i) { return root.frontSearch(i); } public void delete(int i) { if(root.value == i) { root = null; }else { root.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 midShow(int index) { int left = 0; int right = 0; if(data==null||data.length==0) { return; } //当前节点的左节点 if(index*2+1<data.length) { left = index*2+1; midShow(left); } //当前遍历的节点 System.out.print(data[index] + " "); //当前节点的右节点 if(index*2+2<data.length) { right = index*2+2; midShow(right); } } }赫夫曼树也称为最优二叉树,每个叶子节点上都是带权的,使得计算出的权值最小就是最优二叉树;所有带权路径最后计算出的值称为WPL。
赫夫曼树的实现方式:首先将一串数字从小到大排序好,取出最小的两个数组成二叉树,两个数的权值相加,就是他们父节点的权值;再根据父节点的权值,在原来数组中进行排序,再次拿到最小的两个数...不断进行上述操作,形成了赫夫曼数。
实现方法:因为需要不断的进行元素删除与添加,所以需要用到集合。将所有书全部放到集合中,再对这些数进行循环处理,先进行排序,再取出两个最小的树,将他们两的和赋值给创建的新的二叉树;再将这两个树从集合中删除,将新建成的树添加到集合中;又开始重新排序,重新操作。知道集合中只有一个元素。
赫夫曼编码原理分析 在定长编码中,每个字节需要有8位二进制表示,空间浪费很大;使用非定长编码的原理是什么呢?将出现频率高的字节使用简单又少的位数进行表示(相当于权值大),将出现频率低的字节使用相对位数较多的(相当于权值小);所以非定长编码对字节的二进制就可以使用赫夫曼树来进行确定 使用赫夫曼编码后不会出现解码不知道怎么解,因为赫夫曼所有编码都是唯一的,每一个编码不会有重叠,就是不存在某一个编码包含了另一个编码的现象,导致解码出错。 结果:使用了赫夫曼后,就相当于是无损压缩,数据传输的位数大大减少了。
二叉排序树概述 也叫二叉查找树,对于二叉树中的任何一个非叶子节点,要求左子节点比当前节点值小,右子节点比当前节点值大。 特点:查找性能与插入删除性能都适中还不错。
创建二叉排序树 原理:其实就是不断地插入节点,然后进行比较。
删除节点 删除叶子节点,只需要找到父节点,将父节点与他的连接断开即可 删除有一个子节点的就需要将他的子节点换到他现在的位置 删除有两个子节点的节点,需要使用他的前驱节点或者后继节点进行替换,就是左子树最右下方的树和右子树最左边的树;即离节点值最接近的值;(还要注解要去判断这个值有没有右节点,有就要将右节点移上来);
public class BinarySortTree { Node root; /** * 向二叉排序中添加节点 * @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); } } /** * 查找节点 */ public Node search(int value) { if(root==null) { return null; } return root.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==value) { parent.left=null; }else { parent.right=null; } }else if(target.left!=null&&target.right!=null) { //删除右子树中值最小的节点,并且获取到值 int min = deleteMin(target.right); //替换目标节点中的值 target.value = min; }else { if(target.left!=null) { if(parent.left.value==value) { parent.left=target.left; }else { parent.right=target.left; } }else { if(parent.left.value==value) { parent.left=target.right; }else { parent.right=target.right; } } } } } /** * 删除一棵树中最小的节点 * @param right * @return */ private int deleteMin(Node node) { Node target = node; //递归向左找到最小的值 while(target.left!=null) { target = target.left; } if(target.right!=null) { //如果右侧有值就移上来 int num = target.value; target = target.right; return num; }else { //删除最小的节点 delete(target.value); return target.value; } } /** * 搜索父节点 * @param value * @return */ public Node searchParent(int value) { if(root==null) { return null; }else { return root.searchParent(value); } } }计算机的两种存储方式 内存:使用电信号来存储数据,不存在机器操作,所以访问速度非常快;但是造价高,断点后数据丢失,一般作为cpu的高速缓存; 硬盘:硬盘也分为机械硬盘和固态硬盘 机械硬盘:存储介质是磁盘,磁盘固定在主轴上,一个主轴上可以固定若干个磁盘;磁盘也分为上下两面称为盘面;盘面上面每一圈叫做磁道,将磁道分为一个个扇区;而我们的数据存在扇区中;磁盘上面有传动臂称为磁头可进行摆动;根据磁盘的转动还有传动臂的摆动就能读取任何一个扇区的内容; 优点是造价低,容量大断点数据不丢失;缺点是由于存储介质的特点,再加上机械运动的耗时,所以磁盘的速度较慢。 磁盘的预读:由于磁盘的读写速度问题,要尽量减少磁盘的I/O操作。所以磁盘往往不是严格的按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定的长度的数据放到内存中;这样做的理论性依据是计算机科学中著名的局部性原理;当读取一个数据的时候,其附近的数据也通常会拿上被使用。 预读的商都一般为页的整数倍 页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(通常页的大小为4k),主存和磁盘以页为单位交换数据。 文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。
使用B或B+树是为了减少I/O操作;因为I/O操作比较耗时,假设将树的度设置为1024,在600亿元素中最多只需要4次I/O操作就能读取到想要的元素。
2-3树和2-3-4树(都属于B树) 二节点要么有两个子节点,要么没有节点;三节点要么没有节点,要么有三个子节点;不能出现节点不满的情况;而且所有叶子节点都要在同一层;
B树和B+树 B树所拥有的最大的子节点个数对应多少阶;上面说的2-3树,2-3-4树都是B树的特例;B树还有2-3-4-5-6等树; B+树与B树的区别:1、B+树的非叶节点只存储索引不存储数据,所有数据都存到叶子节点中;而B树中每个节点都包含数据和索引;2、B+树的叶子节点最右边的指针指向下一个相邻的叶子节点,就是所有的叶子节点都是连在一起组成一个有序列表。 在mysql中的ISAM索引还有InnoDB索引,一种用了B树,一种用了B+树。
哈希表概述 哈希表也称为散列表,就是一串数组;传入一个数值,这个数值经过在散列(哈希)函数进行计算后,得到的是一个在数组中的具体位置,这个得到的就是hashcode;所以hashcode就是在hash表中有对应的位置;然后通过这个位置就可以将数值存在这个位置上或者找出这个位置上的数,这样的查询速度是O(1)级别的。
散列函数几种设计方法 1、直接定址法 2、数字分析法 通过对数字的分析,找出不太容易重复的部分,或者进行计算,得到结果值; 3、平方取中法 将数字进行平方后的结果取中间的值进行放置; 4、取余法(常用) 对数字进行取余; 5、随机数法
散列冲突的解决方案 1、开放地址发 线性探测法 在探测到的之后紧跟着存 二次探测法 可对数字进行添加或者平方某个数,防止数字聚集 再哈希法 设计多个哈希算法,第一个重了,使用第二个哈希算法 2、链地址法 就是hashmap使用的方法,在数组中的每个元素下有一个链表;