算法与数据结构
文章目录
一、算法相关
1、十大排序算法总结
排序算法 | 平均时间 | 最差时间 | 稳定度 | 额外空间 | 备注 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n²) | 稳定 | O(1) | n小较好 |
插入排序 | O(n²) | O(n²) | 稳定 | O(1) | n小较好 |
选择排序 | O(n²) | O(n²) | 不稳定 | O(1) | n小较好 |
交换排序 | O(n²) | O(n²) | 不稳定 | O(1) | 大部分数据已经排序好 |
快速排序 | O(nlogn) | O(n²) | 不稳定 | O(nlogn) | n大时较好 |
基数排序 | O(nlogrn) | O(nlogrn) | 稳定 | O(n) | n是真数,r是基数 |
桶排序 | O(nlogn) | O(nlogn) | 不稳定 | O(n) | 数据均匀分配到每一个桶中快; |
计数排序 | O(n+k) | O(n+k) | 稳定 | O(n) | n是待排序元素的个数,k是待排序元素中最大值和最小值之差加上1 |
希尔排序 | O(nlogn) | O(n²)1<s<2 | 不稳定 | O(1) | s是所选分组 |
归并排序 | O(nlogn) | O(nlogn) | 稳定 | O(1) | n大时候较好 |
堆排序 | O(nlogn) | O(nlogn) | 不稳定 | O(1) | n大时候较好 |
冒泡排序
冒泡排序(Bubble Sorting)的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序则交换,使值较大的元素逐渐从前移向后部,就象水底下的气泡一样逐渐向上冒。
public class BucketSort {
public static void main(String[] args) {
int[] arrays = {6,3,7,1000,1,3};
for(int i=1;i<arrays.length;i++) {
for(int k=0;k<arrays.length-1;k++) {
if(arrays[k]>arrays[k+1]) {
int tempNum = arrays[k];
arrays[k] = arrays[k+1];
arrays[k+1] = tempNum;
}
}
}
}
public static void print(int[] arrays) {
for(int k=0;k<arrays.length;k++) {
System.out.print(arrays[k]+",");
}
}
}
插入排序
插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴,但它的原理应该是最容易理解的了,因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序和冒泡排序一样,也有一种优化算法,叫做拆半插入。
/**
* 插入排序: 解题思路分布发
* 思想: 先选一个稳定值, 数组其他值和稳定值做比较
* 完成度:已完成
* */
public class InsertSort {
public static void main(String[] args) {
int[] arrays = {6,3,7,1000,1,3};
///1、第一次插入,期望结果:{3,6 ,7,1000,1,3};
for(int j = 1;j<arrays.length;j++) {
for(int i = 0;i<j;i++) {
if(arrays[j]<=arrays[i]) {
int tempNum = arrays[i];
arrays[i] = arrays[j];
arrays[j] = tempNum;
}
}
System.out.println("第"+j+"次找最小值:");
System.out.println(Arrays.toString(arrays));
}
}
}
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
-
交换排序:
快速排序
快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序 n 个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n²) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
快速排序的名字起的是简单粗暴,因为一听到这个名字你就知道它存在的意义,就是快,而且效率高!它是处理大数据最快的排序算法之一了。虽然 Worst Case 的时间复杂度达到了 O(n²),但是人家就是优秀,在大多数情况下都比平均时间复杂度为 O(n logn) 的排序算法表现要更好,可是这是为什么呢,我也不知道。好在我的强迫症又犯了,查了 N 多资料终于在《算法艺术与信息学竞赛》
上找到了满意的答案
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。 但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小, 比复杂度稳定等于 O(nlogn) 的归并排序要小很多。 所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
1、算法步骤
先从数列中取出一个数作为基准数。 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。 再对左右区间重复第二步,直到各区间只有一个数。
/** * 描述:快速排序 * 完成度:已完成 * 时间复杂度: * 空间复杂度: * * 排序思想:快速排序(Quicksort)是对冒泡排序的一种改进。基本思想是:通过一趟排序将要排序的数据分割成独立的两部分, * 其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列 * */ public class QuickSort { public static void main(String[] args) { int[] nums = {5,1,1,2,0,0}; int[] arr = new int[8000000]; for (int i = 0; i < 8000000; i++) { arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数 } long s1 = System.currentTimeMillis(); new QuickSort().sortArray(arr); long s2 = System.currentTimeMillis(); System.out.println("快排耗时:"+(s2-s1)); } public int[] sortArray(int[] nums) { nums = quickSort(nums,0, nums.length-1); return nums; } public int[] quickSort(int[] nums,int left, int right) { //初始化l,r int l = left; int r = right; //左右端值交换中间变量 int tempVal =0; //中间节点 int pivot = nums[(left+right)/2]; //左右2端往中心遍历 while (l<r) { //左边往右遍历 while (nums[l]<pivot) { l++; } //右边往左遍历 while (nums[r]>pivot) { r--; } //当左右2端点相交,说明完成一次值交换 if(l>=r) { break; } tempVal = nums[l]; nums[l] = nums[r]; nums[r] = tempVal; //左边等于pivot,r后移 if(nums[l]==pivot) { r--; } //右边等于pivot,l后移 if(nums[r]==pivot) { l++; } } //防止栈溢出,当l=r条件恒成立 if(l==r) { l++; r--; } //左向右边递归 if(left<r) { quickSort(nums,left, r); } //右向左边递归 if(right>r) { quickSort(nums,l, right); } return nums; } }
基数排序
基数排序(桶排序)介绍:基数排序(radix sort)属于“分配式排序”(distribution sort),又称“桶子法”(bucket sort)或bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用基数排序法是属于稳定性的排序,基数排序法的是效率高的稳定性排序法基数排序(Radix Sort)是桶排序的扩展基数排序是1887年赫尔曼·何乐礼发明的。它是这样实现的:将整数按位数切割成不同的数字,然后按每个位数分别比较。
基数排序基本思想
将所有待比较数值统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。这样说明,比较难理解,下面我们看一个图文解释,理解基数排序的步骤,将数组 {53, 3, 542, 748, 14, 214} 使用基数排序, 进行升序排序。
![1684744709980](C:\Users\hp\Documents\WeChat Files\god8816\FileStorage\Temp\1684744709980.png)
![1684744731546](C:\Users\hp\Documents\WeChat Files\god8816\FileStorage\Temp\1684744731546.png)
![1684744753790](C:\Users\hp\Documents\WeChat Files\god8816\FileStorage\Temp\1684744753790.png)
/** * 基数排序 * 完成度:已完成 * 未理解 * */ public class RadixSort { public static void main(String[] args) { int arr[] = { 53, 3, 542, 748, 14, 214}; // 80000000 * 11 * 4 / 1024 / 1024 / 1024 =3.3G // int[] arr = new int[8000000]; // for (int i = 0; i < 8000000; i++) { // arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数 // } System.out.println("排序前"); Date data1 = new Date(); SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String date1Str = simpleDateFormat.format(data1); System.out.println("排序前的时间是=" + date1Str); radixSort(arr); Date data2 = new Date(); String date2Str = simpleDateFormat.format(data2); System.out.println("排序前的时间是=" + date2Str); System.out.println("基数排序后 " + Arrays.toString(arr)); } //基数排序方法 public static void radixSort(int[] arr) { //根据前面的推导过程,我们可以得到最终的基数排序代码 //1. 得到数组中最大的数的位数 int max = arr[0]; //假设第一数就是最大数 for(int i = 1; i < arr.length; i++) { max = Math.max(max, arr[i]); } //得到最大数是几位数 int maxLength = (max + "").length(); //定义一个二维数组,表示10个桶, 每个桶就是一个一维数组 //说明 //1. 二维数组包含10个一维数组 //2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length //3. 名明确,基数排序是使用空间换时间的经典算法 int buckeNum = 10; int[][] bucket = new int[buckeNum][arr.length]; //为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数 //可以这里理解 //比如:bucketElementCounts[0] , 记录的就是 bucket[0] 桶的放入数据个数 int[] bucketElementCounts = new int[buckeNum]; //这里我们使用循环将代码处理 for(int i = 0 , n = 1; i < maxLength; i++, n *= buckeNum) { //(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位.. for(int j = 0; j < arr.length; j++) { //取出每个元素的对应位的值 int digitOfElement = arr[j] / n % buckeNum; //放入到对应的桶中 bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j]; bucketElementCounts[digitOfElement]++; } //按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组) int index = 0; //遍历每一桶,并将桶中是数据,放入到原数组 for(int k = 0; k < bucketElementCounts.length; k++) { //如果桶中,有数据,我们才放入到原数组 if(bucketElementCounts[k] != 0) { //循环该桶即第k个桶(即第k个一维数组), 放入 for(int l = 0; l < bucketElementCounts[k]; l++) { //取出元素放入到arr arr[index++] = bucket[k][l]; } } //第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!! bucketElementCounts[k] = 0; } } } }
桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:在额外空间充足的情况下,尽量增大桶的数量,使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中,数据范围在桶范围内。对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
1、什么时候最快
当输入的数据可以均匀的分配到每一个桶中。
2、什么时候最慢
当输入的数据被分配到了同一个桶中。
/**
* 描述:桶排序
* 完成度:已完成
* */
public class BarrelSort {
public static void main(String[] args) {
int[] array = {1,1,1,1,1,1,1,1,0,1,2,3,5,4,9,7,8,6,10};
int[] arr = new int[8000000];
for (int i = 0; i < 8000000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}
long s1 = System.currentTimeMillis();
new BarrelSort().barrelSort(arr);
long s2 = System.currentTimeMillis();
System.out.println("桶排序耗时:"+(s2-s1));
}
public int[] barrelSort(int[] array) {
//1、找最大值
int maxNum = Integer.MIN_VALUE;
for(int i=0;i<array.length;i++) {
if(array[i]>maxNum) {
maxNum = array[i];
}
}
//2、创建桶(范围是 max+1,应为数组从0开始)
int[] bucketArray = new int[maxNum+1];
//3、数据和桶编号做映射,如果映射关系成立,给桶标记
for(int i=0;i<array.length;i++) {
int num = array[i];
//4、如果有桶映射有数据给桶编号+1标志,相同的数累加1
bucketArray[num]=bucketArray[num]+1;
}
//5、遍历桶
int n = 0;
for (int i=0;i<bucketArray.length;i++) {
//每个桶有bucketNum个数
int bucketNum = bucketArray[i];
//标记一个桶有多少个元素
for(int k = 0 ;k<bucketNum;k++) {
//array的第n个元素=第N个桶,桶内元素第n个
array[n++] = i;
}
}
return array;
}
}
希尔排序
希尔排序法介绍希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。
希尔排序法基本思想是,希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lRGZoKpG-1685429178424)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20230522161756819.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lnZwbk3D-1685429178425)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20230522161902505.png)]
public class ShellSort {
public static void main(String[] args) {
//int[] arr = { 8, 9, 1, 7, 2, 3, 5, 4, 6, 0 };
// 创建要给80000个的随机的数组
int[] arr = new int[5];
for (int i = 0; i < 5; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}
System.out.println("排序前");
Date data1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(data1);
System.out.println("排序前的时间是=" + date1Str);
shellSort(arr); //交换式
//shellSort2(arr);//移位方式
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);
System.out.println(Arrays.toString(arr));
}
// 使用逐步推导的方式来编写希尔排序
// 希尔排序时, 对有序序列在插入时采用交换法,
// 思路(算法) ===> 代码
public static void shellSort(int[] arr) {
int temp = 0;
int count = 0;
// 根据前面的逐步分析,使用循环处理
for (int gap = arr.length / 2; gap > 0; gap = gap/2) {
for (int i = gap; i < arr.length; i++) {
// 遍历各组中所有的元素(共gap组,每组有个元素), 步长gap
for (int j = i - gap; j >= 0; j = j-gap) {
// 如果当前元素大于加上步长后的那个元素,说明交换
if (arr[j] > arr[j + gap]) {
temp = arr[j];
arr[j] = arr[j + gap];
arr[j + gap] = temp;
}
}
}
}
}
//对交换式的希尔排序进行优化->移位法
public static void shellSort2(int[] arr) {
// 增量gap, 并逐步的缩小增量
for (int gap = arr.length / 2; gap > 0; gap /= 2) {
// 从第gap个元素,逐个对其所在的组进行直接插入排序
for (int i = gap; i < arr.length; i++) {
int j = i;
int temp = arr[j];
if (arr[j] < arr[j - gap]) {
while (j - gap >= 0 && temp < arr[j - gap]) {
//移动
arr[j] = arr[j-gap];
j -= gap;
}
//当退出while后,就给temp找到插入的位置
arr[j] = temp;
}
}
}
}
}
归并排序
归并排序(MERGE-SORT)是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。 归并排序分为2步,拆分和合并。
/**
* 归并排序
* 完成度:已完成
* */
public class MergetSort {
public static void main(String[] args) {
//int arr[] = { 8, 4, 5, 7, 1, 3, 6, 2 }; //
//测试快排的执行速度
// 创建要给80000个的随机的数组
int[] arr = new int[8000000];
for (int i = 0; i < 8000000; i++) {
arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
}
System.out.println("排序前");
Date data1 = new Date();
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date1Str = simpleDateFormat.format(data1);
System.out.println("排序前的时间是=" + date1Str);
int temp[] = new int[arr.length]; //归并排序需要一个额外空间
mergeSort(arr, 0, arr.length - 1, temp);
Date data2 = new Date();
String date2Str = simpleDateFormat.format(data2);
System.out.println("排序前的时间是=" + date2Str);
}
//分+合方法
public static void mergeSort(int[] arr, int left, int right, int[] temp) {
if(left < right) {
int mid = (left + right) / 2; //中间索引
//向左递归进行分解
mergeSort(arr, left, mid, temp);
//向右递归进行分解
mergeSort(arr, mid + 1, right, temp);
//合并
merge(arr, left, mid, right, temp);
}
}
//合并的方法
/**
*
* @param arr 排序的原始数组
* @param left 左边有序序列的初始索引
* @param mid 中间索引
* @param right 右边索引
* @param temp 做中转的数组
*/
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 初始化i, 左边有序序列的初始索引
int j = mid + 1; //初始化j, 右边有序序列的初始索引
int t = 0; // 指向temp数组的当前索引
//(一)
//先把左右两边(有序)的数据按照规则填充到temp数组
//直到左右两边的有序序列,有一边处理完毕为止
while (i <= mid && j <= right) {
//如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
//即将左边的当前元素,填充到 temp数组
//然后 t++, i++
if(arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else { //反之,将右边有序序列的当前元素,填充到temp数组
temp[t] = arr[j];
t += 1;
j += 1;
}
}
//(二)
//把有剩余数据的一边的数据依次全部填充到temp
while( i <= mid) { //左边的有序序列还有剩余的元素,就全部填充到temp
temp[t] = arr[i];
t += 1;
i += 1;
}
while( j <= right) { //右边的有序序列还有剩余的元素,就全部填充到temp
temp[t] = arr[j];
t += 1;
j += 1;
}
//(三)
//将temp数组的元素拷贝到arr
//注意,并不是每次都拷贝所有
t = 0;
int tempLeft = left; //
//第一次合并 tempLeft = 0 , right = 1 // tempLeft = 2 right = 3 // tL=0 ri=3
//最后一次 tempLeft = 0 right = 7
while(tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
堆排序
堆排序基本介绍堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆, 注意 : 没有要求结点的左孩子的值和右孩子的值的大小关系。每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆大顶堆举例说明。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0ABrrFzh-1685429178426)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20230522164203118.png)]
计数排序
计数排序的时间复杂度为O(n+k),其中n是待排序元素的个数,k是待排序元素中最大值和最小值之差加上1。由于计数排序需要额外的O(k)空间来存储计数数组,所以它的空间复杂度也是O(n+k)。对于具有一定范围的整数序列,计数排序是一种快速而简单的排序算法,但对于值域较大且相差较小的数据,则不是很适用。
2、经典算法介绍
递归算法
无处不在,就是自己循环调用自己。
回溯算法
【代码随想录】回溯能够解决的所有问题都能够抽象为树形结构(多叉树)。注意,回溯法解决的是在集合中递归地查找子集,集合大小就是树的宽度,递归的深度就是树的深度。着重注意:在集合中递归查找。 回溯算法剪枝是比较难的一部分。 常见leetcode题:组合问题、切割问题、子集问题、排列问题、棋盘问题、电话号码的字母组合
void backtracking(参数) {//1:函数参数及返回值
if (终止条件) { //2:终止条件
存放结果;
return;
}
//3:回溯搜索逻辑
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
贪心算法
贪婪算法(贪心算法)是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。
贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。
深度优先遍历
从一个点出发,选择一个遍历方向一直遍历到结束。
广度优先遍历
从一个点出发,尝试遍历更多平行节点。直到平行节点遍历完毕。
动态规划算法
利用递归暴利解决一类问题。 动态规划大致分5步: dp数组及下际的含义,递推公式、dp数组如何初始化、遍历顺序、打印dp数组。 常见leetcode题:背包问题、打家劫舍问题、股票问题、子序列问题。 个人感觉是算法中最难的,地推公式这关都不好过。
3、常见算法
二、数据结构介绍
常用的数据结构,字符串,数组,链表,跳跃表,hash,平衡树、红黑树、B树、tire树、LSM树、treap树,有向图、无向图。 数据结构可视化网站推荐
数据结构 | 平均时间 | 最差时间 | 稳定度 |
---|---|---|---|
字符串 | - | - | - |
数组 | - | - | - |
链表 | - | - | - |
Hash | O(1) | O(1) | 稳定 |
跳跃表 | O(logn) | O(n) | 不稳定 |
二叉树 | O(log n) | O(n) | 不稳定 |
大顶堆 | O(logn) | O(logn) | 稳定 |
小顶堆 | O(logn) | O(logn) | 稳定 |
红黑树 | O(log n) | O(log n) | 稳定 |
B树 | O(log n) | O(log n) | 稳定 |
B+树 | O(log n) | O(log n) | 稳定 |
Treap树 | O(log n) | O(n) | 不稳定 |
LSM树 | 写最快O(1),LSM树依赖实现不好给出平均时间复杂度 | O(n) | 不稳定 |
Trie树 | O(k*L) | O(n) | 不稳定 |
无向图 | 最优时间复杂度O(E),平均时间复杂度取决图结构及算法 | O(V+E) | 不稳定 |
有向图 | 不确定O(V+E) | 不确定O(V+E) | 不稳定 |
带权图(也叫网) | 不确定O(V+E) | 不确定O(V+E) | 不稳定 |
1、字符串
字符串是最常用的数据结构,不同遍历算法不同,时间复杂度也不同。
2、数组
数组也是常用的数据结构,分为一维数组、多维数组、稀疏数组;稀疏数组常用于数据压缩数组;比如游戏地图
3、链表
链表分为单链表、双链表;不同遍历算法不同; 场景:LinkedList
4、Hash
Hash是一个时间复杂度最高的数据结构,但是其不能查询相邻数据信息。 场景:hashMap、redis
5、跳跃表
跳跃表结构多表;性能最好时接近hash,平均性能接近平衡树;最差性能接近字符串,是一种不稳定的数据结构; 场景:需要查询相邻数据的结构 redis Zset结构、ConcurrentSkipListMap、ConcurrentSkipListSet。
在跳表中,每个节点包含一个值和一个数组,数组的大小代表该节点所在层数,数组中的每个元素指向该节点在该层的后继节点。跳表中共有level层,每一层都是一条双向链表,头节点head包含level个后继节点,每个后继节点指向该层中对应节点的后继节点。通过这种分层结构,跳表可以在O(log n)的时间内进行增删查等操作,甚至可以实现基于随机化平衡的数据结构。创建一个跳表,首先需要初始化参数,然后通过随机生成节点层数的方式来插入节点,从而维护跳表的平衡性;删除操作与插入操作类似,也需要维护跳表的平衡性;查找操作则通过在每一层的链表中进行二分查找来完成。跳表适合于元素。 import java.util.Random; public class SkipList { private static final int MAX_LEVEL = 32; // 最大层数 private int size; // 元素个数 private int level; // 当前最大层数 private Node head; // 头节点 private Random random; // 随机数生成器 private class Node { int value; Node[] next; public Node(int value, int level) { this.value = value; next = new Node[level]; } } public SkipList() { size = 0; level = 1; head = new Node(0, MAX_LEVEL); random = new Random(); } // 添加元素 public void add(int value) { int newLevel = randomLevel(); Node newNode = new Node(value, newLevel); Node[] update = new Node[level]; Node cur = head; // 找出每层中插入位置的前一个节点 for (int i = level - 1; i >= 0; i--) { while (cur.next[i] != null && cur.next[i].value < value) { cur = cur.next[i]; } if (i < newLevel) { update[i] = cur; } } // 在每层中插入新节点 for (int i = 0; i < newLevel; i++) { newNode.next[i] = update[i].next[i]; update[i].next[i] = newNode; } // 更新元素个数和最大层数 size++; if (newLevel > level) { level = newLevel; } } // 删除元素 public boolean remove(int value) { Node[] update = new Node[level]; Node cur = head; // 找出每层中删除位置的前一个节点 for (int i = level - 1; i >= 0; i--) { while (cur.next[i] != null && cur.next[i].value < value) { cur = cur.next[i]; } update[i] = cur; } // 执行删除操作 if (cur.next[0] != null && cur.next[0].value == value) { for (int i = 0; i < level; i++) { if (update[i].next[i] != cur.next[i]) { break; } update[i].next[i] = cur.next[i].next[i]; } size--; while (level > 1 && head.next[level - 1] == null) { level--; } return true; } return false; } // 查找元素 public boolean contains(int value) { Node cur = head; for (int i = level - 1; i >= 0; i--) { while (cur.next[i] != null && cur.next[i].value < value) { cur = cur.next[i]; } } return cur.next[0] != null && cur.next[0].value == value; } // 随机生成节点层数 private int randomLevel() { int level = 1; while (random.nextInt(2) == 1 && level < MAX_LEVEL) { level++; } return level; } }
6、二叉树
二叉树有很多种类;满二叉树、完全二叉树、顺序存储二叉树、线索化二叉树、平衡二叉树(AVL);我们用最多的是平衡二叉树、顺序存储二叉树、替罪羊树、赫夫曼树。
满二叉树定义:如果该二叉树的所有叶子节点都在最后一层,并且结点总数= 2^n -1 , n 为层数,则我们称为满二叉树。
完全二叉树定义:如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。
平衡二叉树: 平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树, 可以保证查询效率较高。具有以下特点:它是一. 棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。平衡二叉树的常用实现方法有红黑树、AVL、替罪羊树、Treap、伸展树等
其他的;请看韩顺平老师讲解,我们最常用的二叉树就是平衡二叉树、红黑树
7、大顶堆&小顶堆
对于大顶堆和小顶堆,它们的平均时间复杂度与最差时间复杂度都是O(log n),其中n是堆的大小。
大顶堆是一种完全二叉树,满足父节点的值大于或等于左右子节点的值,根节点即为所有节点中的最大值;而小顶堆则是满足父节点的值小于或等于左右子节点的值,根节点即为所有节点中的最小值。在大顶堆和小顶堆中,我们一般使用数组来存储堆,并通过下标计算来访问堆中的节点。
在大顶堆和小顶堆中,有两个关键的操作:插入一个元素和删除最大(小)元素。这两个操作都需要对堆进行调整,以维护堆的性质,使得堆始终符合“最大元素(或最小元素)在顶部”的要求。在堆调整过程中,需要交换父节点和子节点的值,因此,堆的调整过程的时间复杂度取决于堆的深度,即O(log n)。
对于平均时间复杂度与最差时间复杂度的问题,我们需要考虑堆本身的结构。如果堆已经是平衡二叉树,即所有节点的子树高度相差不超过1,那么堆的深度始终为O(log n),堆的操作的时间复杂度也始终为O(log n)。但是,如果堆的结构已经失去平衡,例如插入的元素本身有序,或者插入和删除元素的次数非常不均衡,那么堆的深度可能会达到n,此时,堆的最差时间复杂度为O(n)。
总之,堆在大多数情况下具有较高的时间复杂度,但是,通过维护堆的平衡性、结构优化等方式,我们可以保证堆在各种数据情况下都能够有不错的性能表现。
8、红黑树
红黑树是一种自平衡的二叉查找树;使用频率最高的二叉树; 场景:hashmap;操作系统;nginx
它可以保持树的高度在O(log n)以内。因此,红黑树的平均时间复杂度和最差时间复杂度都为O(log n),其中n为树中节点的数量。
在红黑树中,每个节点都被标记为黑色或红色,根节点是黑色的,红色节点的两个子节点都是黑色的。红黑树的插入、删除、查询等操作大部分都需要通过颜色变换和节点旋转来完成,它们的时间复杂度都是O(log n)。
需要注意的是,虽然使用红黑树可以保持树的高度在O(log n)以内,但实际上树的深度更小的情况下,操作的效率就会更高,因此,红黑树的性能优化不仅局限于保持树的平衡,还需要考虑节点分配方式、缓存、优化代码等方面。
总之,红黑树是一种自平衡的二叉搜索树,它详细考虑了节点的颜色和分布等维度,保证了一定的平衡性,因此其平均时间复杂度和最差时间复杂度均为O(log n)。
9、B树
B树是IO友好型树;其使用场景是:mysql innodb存储引擎下非聚集索引,MyiSam索引引擎,这2者区别前者记录的是主键id,需要从主键查询数据。 后者记录的是数据的硬件地址,直接从目标寻址, 时间复杂度O(1).
B树是一种平衡树,它的结构与红黑树类似,不同之处在于每个节点可以包含多个元素,因此B树更适用于大规模数据存储和文件系统等领域。B树的平均时间复杂度和最差时间复杂度都为O(log n),其中n为树中键值的数量。
在B树中,根节点至少包含两个子节点,每个节点可以包含m-1个元素和m个子节点,其中m为正整数,且m>=2。B树还有一个特性,就是所有同一层节点的元素个数相同,这样可以保证树的平衡性。在B树中,元素按照大小进行排序,且每个元素在子节点中所对应的范围相邻。通过这种方式,我们可以在较短的时间内找到目标元素或者最相近的元素。
B树的插入、删除、查找等操作,都需要遵循其平衡性和元素分配方式,因此其时间复杂度取决于节点的大小和树的层数,平均时间复杂度和最差时间复杂度均为O(log n)。需要注意的是,B树是一种多叉树,其节点包含多个元素和子节点,因此其操作会涉及到较多的节点操作,相比二叉搜索树和自平衡树等数据结构,B树的常数复杂度较高。
总之,B树是一种平衡树,它可以保证较低的时间复杂度,并且在存储大量数据和快速查询时具有重要作用。B树的平均时间复杂度和最差时间复杂度都为O(log n)。
11、B+树
B+树是IO友好型树;其使用场景是:mysql innodb存储引擎下聚集索引,其特点子节点记录了大量的数据,子节点通过双向链表串联起来支持范围查询。
B+树是一种基于B树的一种数据结构,它的平均时间复杂度和最差时间复杂度都是O(log n),其中n为树中键值的数量。
与B树类似,B+树也是一种平衡树,由多个节点组成,每个节点可以包含多个元素和子节点。与B树不同的是,B+树只有叶节点包含key,内部节点只包含key的索引。叶子节点之间通过指针连接成双向链表,可以支持范围查询。
在B+树中,每个节点可以包含m-1个元素和m个子节点,其中m为正整数且m>=2。B+树的特点是,所有非叶子节点只包含key的索引,而叶节点不仅包含key,还包含真正的数据记录。因此,B+树主要用于在外存储器中索引和查找大文件。
B+树的插入、删除、查找等操作与B树类似,也需要保持树的平衡性和节点的大小。由于B+树内部节点只包含key的索引,可以大大增加树的分支,而叶节点是按顺序存储的,这些特性让B+树的查询操作有很好的性能表现。因此,B+树的平均时间复杂度和最差时间复杂度均为O(log n)。
总之,B+树是一种针对大规模数据存储的索引结构,与B树相比具有更好的查询性能和范围查询能力。它的平均时间复杂度和最差时间复杂度都为O(log n)。
12、Treap树
Treap树是一种基于随机化的搜索树,它同时具有搜索树的特点和堆的特点,它的插入、删除、查找的时间复杂度都是O(log n),其中n是Treap树中节点的个数。
Treap树常用于需要在一个数据结构中同时支持插入、删除、查找,且需要保持有序状态的情况下。由于它是通过随机生成优先级来平衡树的深度,因此它可以应对数据的随机化变化,而且在一定程度上可以平衡树的高度,提高树的结构稳定性。
Treap树也被广泛应用于解决随机化问题,例如最近邻搜索、范围查询、文本搜索和最小生成树等。同时,在需要高效执行连续添加或删除多个元素的情况下,Treap树也比其他基于比较的排序方法表现更好。
Treap树中每个节点由一个关键字key和一个优先级priority组成,其中关键字满足搜索树的性质,优先级满足堆的性质。在插入和删除节点时,我们沿着搜索树的路径操作,同时根据随机生成的优先级来维护搜索树和堆的性质,确保树的平衡与较优性。同时,Treap树的查找操作与二叉搜索树类似,也是沿着搜索树路径查找。以上是一个基础的Treap树的示例代码,可根据实际情况进行改造和扩展,例如增加范围查询、关键字排序等功能。 import java.util.Random; public class Treap { private static class Node { int key, priority; Node left, right; public Node(int key) { this.key = key; priority = new Random().nextInt(); } } private Node root; // 插入节点 public void insert(int key) { root = insert(root, key); } private Node insert(Node node, int key) { if (node == null) { return new Node(key); } if (node.key == key) { return node; } if (key < node.key) { node.left = insert(node.left, key); if (node.left.priority > node.priority) { node = rotateRight(node); } } else { node.right = insert(node.right, key); if (node.right.priority > node.priority) { node = rotateLeft(node); } } return node; } // 删除节点 public void remove(int key) { root = remove(root, key); } private Node remove(Node node, int key) { if (node == null) { return null; } if (key < node.key) { node.left = remove(node.left, key); } else if (key > node.key) { node.right = remove(node.right, key); } else if (node.left != null && node.right != null) { if (node.left.priority > node.right.priority) { node = rotateRight(node); node.right = remove(node.right, key); } else { node = rotateLeft(node); node.left = remove(node.left, key); } } else { node = (node.left != null) ? node.left : node.right; } return node; } // 查找节点 public boolean contains(int key) { return contains(root, key); } private boolean contains(Node node, int key) { if (node == null) { return false; } if (node.key == key) { return true; } if (key < node.key) { return contains(node.left, key); } else { return contains(node.right, key); } } // 左旋 private Node rotateLeft(Node node) { Node child = node.right; node.right = child.left; child.left = node; return child; } // 右旋 private Node rotateRight(Node node) { Node child = node.left; node.left = child.right; child.right = node; return child; } }
13、LSM树
一、时间复杂度
LSM树作为一种针对写多读少场景的数据结构,具有不同于传统树型数据结构的特殊的时间复杂度性能。
LSM树的写入操作的时间复杂度主要取决于存储引擎的实现,通常情况下,写入的时间复杂度是O(1)的,因为它只需写入数据到磁盘的顺序文件中即可。但如果树为只写磁盘的模式,则需要进行类似于二分查找的步骤才能找到新插入的值的位置。
LSM树的读取操作分为内存读取和磁盘读取两部分。当然内存读取固然时间复杂度极小,这里主要说一下磁盘读取的时间复杂度。因为数据是以顺序形式存储在磁盘上的,因此LSM树的磁盘读取复杂度不同于B树或者其他平衡树,具体来说,它可以通过大幅减少磁盘I/O次数来提高读取性能。因为读取的过程中,LSM树可以尽可能多地读取磁盘上连续的块文件,从而降低未命中数据的访问代价,提高读取速度。
因此,LSM树的平均时间复杂度不易给出,但是对于最坏情况,LSM树的查找复杂度可能为O(n),其中n是存储的元素总数,因为LSM树需要在磁盘上进行搜索和合并操作。但一般情况下,不考虑最坏情况,LSM树的查询复杂度通常被认为是非常高效和可预测的。
总之,LSM树作为一种分布式数据库系统和大规模写入场景存储的常用数据结构,因其写入速度快、可扩展性强等优点被广泛作为存储介质进行使用。虽然其平均时间复杂度难以衡量,但其本身特性使得其对写入场景和多量数据的情况下具有高效性和极高的可扩展性。
二、使用场景
LSM树(Log Structured Merge Tree)是一种高效的数据结构,适用于写多读少或者写入速度远远高于读取速度的场景。LSM树通常用于分布式数据库系统中,如Cassandra、HBase、LevelDB等。
LSM树的实现方式是将数据存储在不同层次的结构中,每个层次都有其自己的写入和读取策略,并且数据在不同层次之间进行排序和合并。这种设计使得LSM树适用于大量写操作的场景。
LSM树的使用场景包括需要快速写入大量数据,但读取数据的次数比写入数据的次数少的场景。LSM树适用于需要进行流式处理或者批量处理的场景,比如日志存储、数据备份、网络爬虫、物联网等。由于LSM树可以高效地进行批量写入,因此它在实时数据分析、数据抽取和清洗等领域也可以得到应用。
当前许多流行的数据库系统都使用了LSM树的实现,如Apache Cassandra、HBase、LevelDB等。这些数据库采用了LSM树的实现,以优化数据存储和检索,提高性能和可扩展性。LSM树也被用于多种应用中,如建立索引、文本搜索、日志管理、图像处理和数据库崩溃恢复等。
综上所述,LSM树是一种适用于写入速度远高于读取速度的场景,可以提高数据处理的性能和可扩展性。许多流行的数据库系统都采用了LSM树的实现,并且LSM树也被广泛应用于建立索引、文本搜索、日志管理、图像处理和数据库崩溃恢复等领域。
14、Trie树
Trie树是一种专门用于字符串搜索和匹配的树形数据结构,其平均时间复杂度取决于树的高度、节点数和查询字符串的长度,最差时间复杂度取决于查询字符串的长度。
Trie树的平均时间复杂度是O(k*L),其中k是Trie树节点的平均度数,L是查询字符串的长度。度数指的是一个节点拥有的孩子节点数量,对于只考虑26个小写字母的情况,Trie树的度数是26。因此,Trie树的平均时间复杂度主要受节点数的影响,当树的节点数比较多时,Trie树会退化成一个类似链表的结构,此时查询性能会降低。
Trie树的最坏时间复杂度是O(L),其中L是查询字符串的长度。当查询字符串不在Trie树中时,需要遍历整个Trie树才能确定这一点,因此最坏情况下的时间复杂度为O(L)。
对于基于Trie树的优秀算法,如AC自动机,其时间复杂度同样与查询字符串长度有关。AC自动机在最坏情况下的时间复杂度为O(n*L),其中n是Trie树的节点数,L是查询字符串的长度。但是,AC自动机通过维护一个状态转移的有向无环图,可以极大地提高字符串匹配速度,避免不必要的遍历,因此在实际应用中有很高的效率。
综上所述,Trie树的平均时间复杂度取决于树的高度、节点数和查询字符串的长度,最坏时间复杂度取决于查询字符串的长度。基于Trie树的高效算法,如AC自动机,可以极大地提高字符串匹配速度。
15、多叉树
多叉树:2-3树最简单的多叉树;B树;B+树
16、无向图
无向图是一种常见的图形结构,它具有不确定性和连通性等特点,最坏和平均时间复杂度都取决于算法的实现和图形的规模。
在无向图中,最基本的操作是遍历和搜索。遍历一般用于查找无向图中的所有节点或某个联通分量中的节点,而搜索则用于查找无向图中的某个特定节点或某个特定路径。最优时间复杂度是O(E),最坏情况下,无向图的遍历和搜索的时间复杂度都为O(V+E),其中V是无向图中的节点数,E是无向图中边的数量。
另外,在无向图中还有许多特殊的算法,如最短路径算法、最小生成树算法等。最短路径算法的最坏时间复杂度为O(ElogV),其中E是边的数量,V是顶点数量。最小生成树算法的最坏时间复杂度为O(ElogV),其中V和E分别代表无向图中的节点数和边的数量。
需要注意的是,无向图的最坏时间复杂度不代表算法的实际性能,实际应用中的性能取决于许多因素,如图形的规模、算法的复杂度、硬件配置等。因此,在实际应用中,需要根据具体情况选择合适的算法,以及对算法进行优化或并行化等策略,提高算法的运行效率。
综上所述,无向图在最坏情况下,遍历和搜索的时间复杂度是O(V+E),特殊的算法,如最短路径算法和最小生成树算法的最坏时间复杂度为O(E*logV)。但在实际应用中,算法的实际性能取决于许多因素,需要根据具体情况选择合适的算法,以及对算法进行优化或并行化等策略,提高算法的运行效率。
17、有向图
JVM堆对象之间的引用结构就是一种无向图结构;
有向图的时间复杂度取决于所采用的算法、图的规模和结构。以下是几种常见的有向图算法的时间复杂度:
有向图遍历:使用DFS或BFS等简单的遍历算法,时间复杂度为O(V+E),其中V为节点数,E为边数。
拓扑排序:使用DFS或BFS等简单的遍历算法,时间复杂度为O(V+E),其中V为节点数,E为边数。
最短路径问题:使用Dijkstra算法,平均时间复杂度为O((V+E)logV);使用Bellman-Ford算法,平均时间复杂度为O(VE);使用Floyd算法,平均时间复杂度为O(V^3)。
图的连通性问题:使用DFS或BFS等简单的遍历算法,时间复杂度为O(V+E)。可以使用Tarjan算法或Kosaraju算法查找强连通分量,时间复杂度为O(V+E)。
最小路径覆盖问题:使用Ford-Fulkerson算法或Dinic算法,时间复杂度为O(E^2*V),其中E为边数,V为节点数。
最大流问题:使用Edmonds-Karp算法、Dinic算法或Push-Relabel算法,时间复杂度为O(E^2*V),其中E为边数,V为节点数。
需要注意的是,以上算法的时间复杂度均为最差情况下的复杂度。在实践中,应根据具体的应用场景和数据规模选择合适的算法,以获得更好的性能。
18、带权图(网)
带权图的时间复杂度取决于所采用的算法、图的规模和权重分布。以下是几种常见的带权图算法的时间复杂度:
最短路径问题:使用Dijkstra算法,平均时间复杂度为O((E+V)logV),其中E为边数,V为节点数;使用Bellman-Ford算法,时间复杂度为O(VE);使用A*算法,平均时间复杂度更优。
最小生成树问题:使用Prim算法或Kruskal算法,时间复杂度为O(V^2)或O(ElogE),其中V为节点数,E为边数。
单源最短路径问题:使用Dijkstra算法,时间复杂度为O(E+VlogV);使用Bellman-Ford算法,时间复杂度为O(VE)。
多源最短路径问题:使用Floyd算法,时间复杂度为O(V^3)。
最小权闭合子图问题:使用网络流算法,时间复杂度为O(EVlogV)。
需要注意的是,这些算法的具体时间复杂度仍然受到具体问题规模和数据分布等因素的影响。在实际应用中,应根据具体情况选择合适的算法,以获得更好的性能。
图结构;有向图的时间复杂度取决于所采用的算法、图的规模和结构。以下是几种常见的有向图算法的时间复杂度:
有向图遍历:使用DFS或BFS等简单的遍历算法,时间复杂度为O(V+E),其中V为节点数,E为边数。
拓扑排序:使用DFS或BFS等简单的遍历算法,时间复杂度为O(V+E),其中V为节点数,E为边数。
最短路径问题:使用Dijkstra算法,平均时间复杂度为O((V+E)logV);使用Bellman-Ford算法,平均时间复杂度为O(VE);使用Floyd算法,平均时间复杂度为O(V^3)。
图的连通性问题:使用DFS或BFS等简单的遍历算法,时间复杂度为O(V+E)。可以使用Tarjan算法或Kosaraju算法查找强连通分量,时间复杂度为O(V+E)。
最小路径覆盖问题:使用Ford-Fulkerson算法或Dinic算法,时间复杂度为O(E^2*V),其中E为边数,V为节点数。
最大流问题:使用Edmonds-Karp算法、Dinic算法或Push-Relabel算法,时间复杂度为O(E^2*V),其中E为边数,V为节点数。
需要注意的是,以上算法的时间复杂度均为最差情况下的复杂度。在实践中,应根据具体的应用场景和数据规模选择合适的算法,以获得更好的性能。
18、带权图(网)
带权图的时间复杂度取决于所采用的算法、图的规模和权重分布。以下是几种常见的带权图算法的时间复杂度:
最短路径问题:使用Dijkstra算法,平均时间复杂度为O((E+V)logV),其中E为边数,V为节点数;使用Bellman-Ford算法,时间复杂度为O(VE);使用A*算法,平均时间复杂度更优。
最小生成树问题:使用Prim算法或Kruskal算法,时间复杂度为O(V^2)或O(ElogE),其中V为节点数,E为边数。
单源最短路径问题:使用Dijkstra算法,时间复杂度为O(E+VlogV);使用Bellman-Ford算法,时间复杂度为O(VE)。
多源最短路径问题:使用Floyd算法,时间复杂度为O(V^3)。
最小权闭合子图问题:使用网络流算法,时间复杂度为O(EVlogV)。
需要注意的是,这些算法的具体时间复杂度仍然受到具体问题规模和数据分布等因素的影响。在实际应用中,应根据具体情况选择合适的算法,以获得更好的性能。