数据结构和算法
数据结构
数据结构包括:线性结构和非线性结构。
线性结构
-
线性结构作为最常用的数据结构, 其特点是数据元素之间存在一对一的线性关系
-
线性结构有两种不同的存储结构, 即顺序存储结构(数组)和链式存储结构(链表)。 顺序存储的线性表称为顺序表, 顺序表中的存储元素是连续的
-
链式存储的线性表称为链表, 链表中的存储元素不一定是连续的, 元素节点中存放数据元素以及相邻元素的地址信息
-
线性结构常见的有: 数组、 队列、 链表和栈, 后面我们会详细讲解.
非线性结构
非线性结构包括: 二维数组, 多维数组, 广义表, 树结构, 图结构
稀疏数组和队列
稀疏数组
当一个数组中大部分元素为0 , 或者为同一个值的数组时, 可以使用稀疏数组来保存该数组。
稀疏数组的处理方法是:
- 记录数组一共有几行几列, 有多少个不同的值
- 把具有不同值的元素的行列及值记录在一个小规模的数组中, 从而缩小程序的规模
队列
-
队列是一个有序列表, 可以用数组或是链表来实现。
-
遵循先入先出的原则。 即: 先存入队列的数据, 要先取出。 后存入的要后取出
-
示意图: (使用数组模拟队列示意图)
链表
- 链表是以节点的方式来存储,是链式存储
- 每个节点包含 data 域, next 域: 指向下一个节点.
- 如图: 发现链表的各个节点不一定是连续存储.
- 链表分带头节点的链表和没有头节点的链表, 根据实际的需求来确定
栈
-
栈的英文为(stack)
-
栈是一个先入后出(FILO-First In Last Out)的有序列表。
-
栈(stack)是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。 允许插入和删除的一端, 为变化的一端,
称为栈顶(Top), 另一端为固定的一端, 称为栈底(Bottom)。
- 根据栈的定义可知, 最先放入栈中元素在栈底, 最后放入的元素在栈顶, 而删除元素刚好相反, 最后放入的元素最先删除, 最先放
入的元素最后删除
- 图解方式说明出栈(pop)和入栈(push)的概念
栈实现综合计算器(中缀表达式)
一个字符串,我们需要使用栈的数据结构来计算出他的值:
例如:String str="2×3+3×7";
我们使用一个数据栈,一个符号栈
指针到2,压入数据站,指针到×压入符号栈,指针到3压入数据栈,指针到+,判断他在符号栈中的优先级。他的优先级低,就从数据栈里弹出两个数,符号栈里弹出一个符号计算出结果。压入数据栈,符号优先级没有低的了。就压入符号栈。
此时数据栈中的数据是,6,3,7。符号栈中的符号是+,×。
最后是直接弹出数据和符号,进行计算。
前缀,中缀,后缀表达式。
前缀表达式(波兰表达式)
- 前缀表达式又称波兰式,前缀表达式的运算符位于操作数之前
- 举例说明: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6
前缀表达式的计算机求值
从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果
例如: (3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下:
从右至左扫描,将6、5、4、3压入堆栈
遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈
接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈
最后是-运算符,计算出35-6的值,即29,由此得出最终结果
中缀表达式
中缀表达式就是常见的运算表达式,如(3+4)×5-6
中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(前面我们讲的案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式.)
后缀表达式
后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后
中举例说明: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –
再比如:
后缀表达式的计算机求值
从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下:
从左至右扫描,将3和4压入堆栈;
遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈;
将5入栈;
接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;
将6入栈;
最后是-运算符,计算出35-6的值,即29,由此得出最终结果
中缀表达式与后缀表达式的转换。
大家看到,后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,我们需要将 中缀表达式转成后缀表达式。
具体步骤如下:
初始化两个栈:运算符栈s1和储存中间结果的栈s2;
从左至右扫描中缀表达式;
遇到操作数时,将其压s2;
遇到运算符时,比较其与s1栈顶运算符的优先级:
如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;
若优先级比栈顶运算符的高,也将运算符压入s1;
如果先来的符号比符号栈栈顶符号优先级低的话,将符号栈对的高优先级运算符弹出,如果遇到相等优先级的也继续弹出,只要遇到符号栈中的符号比他低,才压入。
遇到括号时:
(1) 如果是左括号“(”,则直接压入s1
(2) 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号为止,此时将这一对括号丢弃
重复步骤2至5,直到表达式的最右边
将s1中剩余的运算符依次弹出并压入s2
依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
具体的求解过程如下:
一、手工方式
如果只是用于解题的话,这种方法是最快最准确的。但是它不适用于计算机。下面以a+b*c+(d*e+f)*g
为例子讲以下人应该怎么把中缀表达式转换成后缀表达式。
-
按先加减后乘除的原则给表达式加括号
结果:
((a+(b*c))+(((d*e)+f)*g))
-
由内到外把每个括号里的表达式换成后缀
最终结果:
abc*+de*f+g*+
这样就得到了中缀表达式转后缀表达式的最终结果。此法应付考试有神效。
二、栈的方式
计算机毕竟没有人的思路,需要借助栈来实现从中缀表达式到后缀表达式的转换。
这里明确一下使用栈转换的算法思想
从左到右开始扫描中缀表达式
遇到数字, 直接输出
遇到运算符
a.若为“(” 直接入栈
b.若为“)” 将符号栈中的元素依次出栈并输出, 直到 “(“, “(“只出栈, 不输出
c.若为其他符号, 将符号栈中的元素依次出栈并输出, 直到遇到比当前符号优先级更低的符号或者”(“。 将当前符号入栈。
扫描完后, 将栈中剩余符号依次输出
下面以a+b*c+(d*e+f)*g
为例子来讲讲计算机的转换过程。下面在描述栈的情况是直接用文字描述了,由左到右为栈底到栈顶。空表示栈空
-
由左向右遍历表达式,首先遇到a,直接将其输出。
此时输出为:
a
栈的情况为:空
-
继续遍历,遇到+,将其放入栈中。
此时输出为:
a
栈的情况为:+
-
继续遍历,遇到b,直接将其输出。
此时输出为:
ab
栈的情况为:+
-
继续遍历,遇到,因为的优先级大于栈顶的+,所以将*放入栈内。
此时输出为:
ab
栈的情况为:+*
-
继续遍历,遇到c,直接将其输出。
此时输出为:
abc
栈的情况为:+*
-
继续遍历,遇到+,因为+的优先级低于栈顶的,故将弹出;然后新的栈顶元素的+与这个+优先级相同,故也要弹出现在栈顶的+;然后栈空了,将现在这个+放入栈中。
此时输出为:
abc*+
栈的情况为:+
-
继续遍历,遇到(,直接将其放入栈中,不遇到)不会将(弹出。
此时输出为:
abc*+
栈的情况为:+(
-
继续遍历,遇到d,直接将其输出。
此时输出为:
abc*+d
栈的情况为:+(
-
继续遍历,遇到,因为栈顶为(,不遇到)不将(弹出,故直接将放入栈中。
此时输出为:
abc*+d
栈的情况为:+(*
-
继续遍历,遇到e,直接将其输出。
此时输出为:
abc*+de
栈的情况为:+(*
-
继续遍历,遇到+,因为+比栈顶的优先级低,故将弹出;新的栈顶元素为(,不遇到)不弹出(,故将+放入栈中。
此时输出为:
abc*+de*
栈的情况为:+(+
-
继续遍历,遇到f,直接将其输出。
此时输出为:
abc*+de*f
栈的情况为:+(+
-
继续遍历,遇到),直接将栈中元素依次弹出并输出直到遇到(为止,注意:(弹出但不输出。
此时输出为:
abc*+de*f+
栈的情况为:+
-
继续遍历,遇到,因为的优先级大于栈顶元素+的优先级,故直接将*入栈。
此时输出为:
abc*+de*f+
栈的情况为:+*
-
继续遍历,遇到g,直接将其输出。
此时输出为:
abc*+de*f+g
栈的情况为:+*
-
继续遍历,为空,遍历结束。将栈内元素依次弹出。
此时输出为:
abc*+de*f+g*+
栈的情况为:空
至此,中缀表达式转后缀已经全部完成,结果为abc\*+de\*f+g\*+
。
递归
简单的说: 递归就是方法自己调用自己,每次调用时传入不同的变量.递归有助于编程者解决复杂的问题,同时 可以让代码变得简洁
递归需要遵守的重要规则
-
执行一个方法时, 就创建一个新的受保护的独立空间(栈空间)
-
方法的局部变量是独立的, 不会相互影响, 比如 n 变量
-
如果方法中使用的是引用类型变量(比如数组), 就会共享该引用类型的数据.
-
递归必须向退出递归的条件逼近, 否则就是无限递归,出现 StackOverflowError, 死龟了:)
-
当一个方法执行完毕, 或者遇到 return, 就会返回, 遵守谁调用, 就将结果返回给谁, 同时当方法执行完毕或者返回时, 该方法也就执行完毕
排序算法
排序也称排序算法(Sort Algorithm),排序是将一组数据,依指定的顺序进行排列的过程。
7.2 排序的分类:
-
内部排序: 指将需要处理的所有数据都加载到内部存储器(内存)中进行排序。
-
外部排序法: 数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序。
-
常见的排序算法分类(见右图)
时间复杂度
- 一般情况下,算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数,
常见的时间复杂度
-
常数阶 O(1)
-
对数阶 O(log2n)
-
线性阶 O(n)
-
线性对数阶 O(nlog2
-
平方阶 O(n^2)
-
立方阶 O(n^3)
-
k 次方阶 O(n^k)
-
指数阶 O(2^n)
冒泡排序
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较 相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
优化: 因为排序的过程中,各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在 排序过程中设置一个标志 flag 判断元素是否进行过交换。从而减少不必要的比较。(这里说的优化,可以在冒泡排 序写好后,在进行)
import java.text.SimpleDateFormat;
import java.util.Date;
public class BubbleSort {
@Test
public void t1() {
int[] ints = {13, 34, 4, 2, 356, 67};
bubbleSort(ints);
for (int anInt : ints) {
System.out.println(anInt);
}
}
public static void main(String[] args) {
// int arr[] = {3, 9, -1, 10, 20};
//
// System.out.println("排序前");
// System.out.println(Arrays.toString(arr));
//为了容量理解,我们把冒泡排序的演变过程,给大家展示
//测试一下冒泡排序的速度O(n^2), 给80000个数据,测试
//创建要给80000个的随机的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); //生成一个[0, 8000000) 数
}
Date data1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(data1);
System.out.println("排序前的时间是=" + date1Str);
//测试冒泡排序
bubbleSort(arr);
// for (int i : arr) {
// System.out.println("+" + i);
// }
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序后的时间是=" + date2Str);
//System.out.println("排序后");
//System.out.println(Arrays.toString(arr));
/*
// 第二趟排序,就是将第二大的数排在倒数第二位
for (int j = 0; j < arr.length - 1 - 1 ; j++) {
// 如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第二趟排序后的数组");
System.out.println(Arrays.toString(arr));
// 第三趟排序,就是将第三大的数排在倒数第三位
for (int j = 0; j < arr.length - 1 - 2; j++) {
// 如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第三趟排序后的数组");
System.out.println(Arrays.toString(arr));
// 第四趟排序,就是将第4大的数排在倒数第4位
for (int j = 0; j < arr.length - 1 - 3; j++) {
// 如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
System.out.println("第四趟排序后的数组");
System.out.println(Arrays.toString(arr)); */
}
// 将前面额冒泡排序算法,封装成一个方法
public static void bubbleSort(int[] arr) {
// 冒泡排序 的时间复杂度 O(n^2), 自己写出
int temp = 0; // 临时变量
boolean flag = false; // 标识变量,表示是否进行过交换
for (int i = 0; i < arr.length - 1; i++) {
for (int j = 0; j < arr.length - 1 - i; j++) {
// 如果前面的数比后面的数大,则交换
if (arr[j] > arr[j + 1]) {
flag = true;
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
//System.out.println("第" + (i + 1) + "趟排序后的数组");
//System.out.println(Arrays.toString(arr));
if (!flag) { // 在一趟排序中,一次交换都没有发生过
break;
} else {
flag = false; // 重置flag!!!, 进行下次判断
}
}
}
}
选择排序
选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到 排序的目的
选择排序的本质是,第一趟,在第一个位置,选出最大或最小值,第二趟在第二个位置,选出最大或者最小值。一直循环。
import java.text.SimpleDateFormat;
import java.util.Date;
//选择排序
public class SelectSort {
public static void main(String[] args) {
//int [] arr = {101, 34, 119, 1, -1, 90, 123};
//创建要给80000个的随机的数组
int[] arr = new int[80000];
for (int i = 0; i < 80000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}
System.out.println("排序前");
//System.out.println(Arrays.toString(arr));
Date data1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(data1);
System.out.println("排序前的时间是=" + date1Str);
selectSort(arr);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);
//System.out.println("排序后");
//System.out.println(Arrays.toString(arr));
}
//选择排序
public static void selectSort(int[] arr) {
//在推导的过程,我们发现了规律,因此,可以使用for来解决
//选择排序时间复杂度是 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[0], 即交换
if (minIndex != i) {
arr[minIndex] = arr[i];
arr[i] = min;
}
//System.out.println("第"+(i+1)+"轮后~~");
//System.out.println(Arrays.toString(arr));// 1, 34, 119, 101
}
/*
//使用逐步推导的方式来,讲解选择排序
//第1轮
//原始的数组 : 101, 34, 119, 1
//第一轮排序 : 1, 34, 119, 101
//算法 先简单--》 做复杂, 就是可以把一个复杂的算法,拆分成简单的问题-》逐步解决
//第1轮
int minIndex = 0;
int min = arr[0];
for(int j = 0 + 1; j < arr.length; j++) {
if (min > arr[j]) { //说明假定的最小值,并不是最小
min = arr[j]; //重置min
minIndex = j; //重置minIndex
}
}
//将最小值,放在arr[0], 即交换
if(minIndex != 0) {
arr[minIndex] = arr[0];
arr[0] = min;
}
System.out.println("第1轮后~~");
System.out.println(Arrays.toString(arr));// 1, 34, 119, 101
//第2轮
minIndex = 1;
min = arr[1];
for (int j = 1 + 1; j < arr.length; j++) {
if (min > arr[j]) { // 说明假定的最小值,并不是最小
min = arr[j]; // 重置min
minIndex = j; // 重置minIndex
}
}
// 将最小值,放在arr[0], 即交换
if(minIndex != 1) {
arr[minIndex] = arr[1];
arr[1] = min;
}
System.out.println("第2轮后~~");
System.out.println(Arrays.toString(arr));// 1, 34, 119, 101
//第3轮
minIndex = 2;
min = arr[2];
for (int j = 2 + 1; j < arr.length; j++) {
if (min > arr[j]) { // 说明假定的最小值,并不是最小
min = arr[j]; // 重置min
minIndex = j; // 重置minIndex
}
}
// 将最小值,放在arr[0], 即交换
if (minIndex != 2) {
arr[minIndex] = arr[2];
arr[2] = min;
}
System.out.println("第3轮后~~");
System.out.println(Arrays.toString(arr));// 1, 34, 101, 119 */
}
}
插入排序
插入排序法介绍
插入式排序属于内部排序法, 是对于欲排序的元素以插入的方式找寻该元素的适当位置, 以达到排序的目的。
插入排序法思想:
插入排序(Insertion Sorting) 的基本思想是: 把 n 个待排序的元素看成为一个有序表和一个无序表, 开始时有序表中只包含一个元素, 无序表中包含有 n-1 个元素, 排序过程中每次从无序表中取出第一个元素, 把它的排序码依次与有序表元素的排序码进行比较, 将它插入到有序表中的适当位置, 使之成为新的有序表。
希尔排序法
希尔排序是希尔(Donald Shell) 于 1959 年提出的一种排序算法。 希尔排序也是一种插入排序, 它是简单插入排序经过改进之后的一个更高效的版本, 也称为缩小增量排序。
- 希尔排序时, 对有序序列在插入时采用交换法, 并测试排序速度.
- 希尔排序时, 对有序序列在插入时采用移动法, 并测试排序速度
希尔排序,
快速排序法
快速排序(Quicksort) 是对冒泡排序的一种改进。 基本思想是: 通过一趟排序将要排序的数据分割成独立的两部分, 其中一部分的所
有数据都比另外一部分的所有数据都要小, 然后再按此方法对这两部分数据分别进行快速排序, 整个排序过程可以递归进行, 以此达到
整个数据变成有序序列
归并排序
归并排序(MERGE-SORT) 是利用归并的思想实现的排序方法, 该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分
(divide)成一些小的问题然后递归求解, 而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起, 即分而治之)
基数排序
将所有待比较数值统一为同样的数位长度, 数位较短的数前面补零。 然后, 从最低位开始, 依次进行一次排序。这样从最低位排序一直
到最高位排序完成以后, 数列就变成一个有序序列。
常用排序算法总结和对比
备注
选择排序比冒泡排序的效率高几倍
这几种排序方式的速度
冒泡排序,选择排序,插入排序,时间复杂度都是o(n²)
希尔排序,快速排序,归并排序,时间复杂度都是o(logn)
基数排序的时间复杂度时o(n✖k)
如果数据的基数很大,那么基数排序最快,但是基数排序消耗能存,如果基数不是很大,那么快排,归并,希尔速度块。
查找算法
在 java 中, 我们常用的查找有四种:
-
顺序(线性)查找
-
二分查找/折半查找
-
插值查找
-
斐波那契查找
二分查找就是中间对折查找。
插值查找,就是改变了查找的位置,不是中间对折,而是换了参数.(插值查找的公式:int mid = left + (right – left) * (findVal – arr[left]) / (arr[right] – arr[left]) ),这个mid的计算,其实对数据一开始就在首尾,进行了排除。
斐波那契也是一样,参数是0.618
注意事项:
- 对于数据量较大, 关键字分布比较均匀的查找表来说, 采用插值查找, 速度较快.
- 关键字分布不均匀的情况下, 该方法不一定比折半查找要好
二分查找
哈希表
散列表(Hash table, 也叫哈希表) , 是根据关键码值(Key value)而直接进行访问的数据结构。 也就是说, 它通过把关键码值映射到
表中一个位置来访问记录, 以加快查找的速度。 这个映射函数叫做散列函数, 存放记录的数组叫做散列表。
树
树这种数据结构
- 数组存储方式的分析
优点: 通过下标方式访问元素, 速度快。 对于有序数组, 还可使用二分查找提高检索速度。
缺点: 如果要检索具体某个值, 或者插入值(按一定顺序)会整体移动, 效率较低
- 链式存储方式的分析
优点: 在一定程度上对数组存储方式有优化(比如: 插入一个数值节点, 只需要将插入节点, 链接到链表中即可,删除效率也很好)。
缺点: 在进行检索时, 效率仍然较低, 比如(检索某个值, 需要从头节点开始遍历)
- 树存储方式的分析
能提高数据存储, 读取的效率, 比如利用 二叉排序树(Binary Sort Tree), 既可以保证数据的检索速度, 同时也可以保证数据的插入, 删
除, 修改的速度。
二叉树的概念
-
树有很多种, 每个节点最多只能有两个子节点的一种形式称为二叉树。
-
如果该二叉树的所有叶子节点都在最后一层, 并且结点总数= 2^n -1 , n 为层数, 则我们称为满二叉树。
-
如果该二叉树的所有叶子节点都在最后一层或者倒数第二层, 而且最后一层的叶子节点在左边连续, 倒数第二层的叶子节点在右边连续, 我们称为完全二叉树
二叉树的遍历情况:
前序遍历:中,左,右
中序遍历:坐中右
后续变量:左右中
树结构实际应用
堆排序
是对树形结构数据的一种实际应用。
11.1 堆排序 11.1.1 堆排序基本介绍
-
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复 杂度均为 O(nlogn),它也是不稳定排序。
-
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有 要求结点的左孩子的值和右孩子的值的大小关系。
-
每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
-
大顶堆举例说明
- 一般升序采用大顶堆,降序采用小顶堆
堆排序的速度非常快。800w的数据,只用3秒。
11.2 赫夫曼树
11.2.1 基本介绍
-
给定 n 个权值作为 n 个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为 最优二叉树,也称为哈夫曼树(Huffman Tree), 还有的书翻译为霍夫曼树。
-
赫夫曼树是带权路径长度最短的树,权值较大的结点离根较近
赫夫曼树几个重要概念和举例说明
- 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路
中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1
-
结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结 点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积
-
树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为 WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。
赫夫曼树主要是用在了通信领域的数据压缩方面。可以大幅度的压缩传输的数据。压缩率达到了60%-90%
为什么赫夫曼树可以大幅度的压缩数据,主要是属性形成赫夫曼树之后,可以出现唯一的编码。而且这个编码不会有歧义性,也有就是UTF8编码和GBK编码类似的问题。
二叉排序树(二叉查找树(BST))
二叉排序树: BST: (Binary Sort(Search) Tree), 对于二叉排序树的任何一个非叶子节点, 要求左子节点的值比当前节点的值小, 右子节
点的值比当前节点的值大。
特别说明: 如果有相同的值, 可以将该节点放在左子节点或右子节点
平衡二叉树(AVL)
二叉平衡树主要是为了解决二叉查找树的所有数据一边倒的情况,也就是单链表。1->2->3->4->5.这种情况下,查找依旧很慢。
- 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree) 又被称为 AVL 树, 可以保证查询效率较高。
- 具有以下特点: 它是一 棵空树或它的左右两个子树的高度差的绝对值不超过 1, 并且左右两个子树都是一棵
平衡二叉树。 平衡二叉树的常用实现方法有红黑树、 AVL、 替罪羊树、 Treap、 伸展树等。 - 举例说明, 看看下面哪些 AVL 树, 为什么?
个人理解:平衡二叉树其实是对二叉排序树的一种转换。例子如下:
但是在我们旋转左右平衡的时候,也是出现了问题,就是有可能左右旋转还是不平衡,例如下面这种情况, 就是旋转了还是有问题:
解决办法:判断要要旋转的那个节点的左右节点的高度,如果高度差的绝对值大于1,那么就在这个节点下做好旋转再进行上一层的旋转。
多路查找树
二叉树的问题分析
二叉树的操作效率较高, 但是也存在问题, 请看下面的二叉树
- 二叉树需要加载到内存的, 如果二叉树的节点少, 没有什么问题, 但是如果二叉树的节点很多(比如 1 亿), 就
存在如下问题: - 问题 1: 在构建二叉树时, 需要多次进行 i/o 操作(海量数据存在数据库或文件中), 节点海量, 构建二叉树时,
速度有影响 - 问题 2: 节点海量, 也会造成二叉树的高度很大, 会降低操作速度.
个人理解:二叉树的高度太高,最后还是影响查找速度,出来了一个多叉树,就是为了降低层数。
-
在二叉树中, 每个节点有数据项, 最多有两个子节点。 如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)
-
后面我们讲解的 2-3 树, 2-3-4 树就是多叉树, 多叉树通过重新组织节点, 减少树的高度, 能对二叉树进行优化。
-
举例说明(下面 2-3 树就是一颗多叉树)
B 树的基本介绍
所有叶子节点都在同一层
2-3 树是最简单的 B 树结构
具有如下特点:
-
2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
-
有两个子节点的节点叫二节点, 二节点要么没有子节点, 要么有两个子节点.
-
有三个子节点的节点叫三节点, 三节点要么没有子节点, 要么有三个子节点.
-
2-3 树是由二节点和三节点构成的树。
插入规则:
- 2-3 树的所有叶子节点都在同一层.(只要是 B 树都满足这个条件)
- 有两个子节点的节点叫二节点, 二节点要么没有子节点, 要么有两个子节点.
- 有三个子节点的节点叫三节点, 三节点要么没有子节点, 要么有三个子节点
- 当按照规则插入一个数到某个节点时, 不能满足上面三个要求, 就需要拆, 先向上拆, 如果上层满, 则拆本层,
拆后仍然需要满足上面 3 个条件。 - 对于三节点的子树的值大小仍然遵守(BST 二叉排序树)的规则
除了 23 树, 还有 234 树等, 概念和 23 树类似, 也是一种 B 树。 如图
B 树、 B+树和 B*树
B-tree 树即 B 树, B 即 Balanced,
B树的非叶子节点也存放数据,
B+树是数据放在了叶子节点。
B*树 在非根,非叶子节点再增加了指向兄弟的指针。
图
图存在的意义:
- 前面我们学了线性表和树
- 线性表局限于一个直接前驱和一个直接后继的关系
- 树也只能有一个直接前驱也就是父节点
- 当我们需要表示多对多的关系时, 这里我们就用到了图。
图是一种数据结构, 其中结点可以具有零个或多个相邻元素。 两个结点之间的连接称为边。 结点也可以称为顶点。 如图:
图的基本概念:
-
顶点(vertex)
-
边(edge)
-
路径
-
无向图(右图
-
有向图
-
带权图
13.2 图的表示方式
图的表示方式有两种: 二维数组表示(邻接矩阵) ; 链表表示(邻接表) 。
所谓图的遍历, 即是对结点的访问。
一个图有那么多个结点, 如何遍历这些结点, 需要特定策略, 一般有两种
访问策略: (1)深度优先遍历 (2)广度优先遍历
程序员常用 10 种算法
二分查找算法(非递归)
分治算法
动态规划算法
求一个背包里放什么东西,最值钱。
0/1背包问题。
KMP算法
KMP 是一个解决模式串在文本串是否出现过, 如果出现过, 最早出现的位置的经典算法
Knuth-Morris-Pratt 字符串查找算法,
https://www.cnblogs.com/zzuuoo666/p/9028287.html
主要是找到字符匹配表,没有弄明白字符匹配表,但是知道了KMP算法了。
贪心算法
贪婪算法(贪心算法)是指在对问题进行求解时, 在每一步选择中都采取最好或者最优(即最有利)的选择, 从而希望能够导致结果是最好或者最优的算法
有一个例子是求广播电台的覆盖问题
普里姆算法
例子是村庄修路。要求所有的村都通路,但是要求历程最短。通过无向带权重的图来做的。
克鲁斯卡尔算法
公交车问题,
-
克鲁斯卡尔(Kruskal)算法, 是用来求加权连通图的最小生成树的算法。
-
基本思想: 按照权值从小到大的顺序选择 n-1 条边, 并保证这 n-1 条边不构成回路
-
具体做法: 首先构造一个只含 n 个顶点的森林, 然后依权值从小到大从连通网中选择边加入到森林中, 并使森林中不产生回路, 直至森林变成一棵树为止