数据结构表示数据在计算机中的存储和组织形式,主要描述数据元素之间和位置关系等。选择适当的数据结构可以提高计算机程序的运行效率(时间复杂度)和存储效率(空间复杂度)。(数据结构研究的内容:就是如何按一定的逻辑结构,把数据组织起来,并选择适当的存储表示方法把逻辑结构组织好的数据存储到计算机的存储器里。)
我们可以从两个维度来理解数据结构:逻辑结构 + 存储结构。
-
逻辑结构 是从数据之间的逻辑关系,大概可以分为两种:线性结构和非线性结构。
常用的线性结构有:一维数组、栈、队列、链表。
常用的非线性结构有:树、图、多维数组
-
存储结构 是逻辑结构用计算机语言的实现,数据之间的存储关系。常见的存储结构有顺序存储、链式存储、索引存储以及散列存储。
说明:数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合。
优点:查找快,时间复杂度为 O(1)
缺点:新增或删除慢,时间复杂度为 O(n);
案例:
- 88.合并两个有序数组 [数组] [解决方案:合并后排序] [2021/11/9] [简单]
- JZ3 数组中重复的数字 [数组] [解决方案:哈希] [2021/11/11] [简单]
- JZ29 顺时针打印矩阵 (同 LeetCode 54. 螺旋矩阵)[数组] [解决方案:按层模拟] [2021/11/14] [中等]
- JZ57 和为S的两个数字 [数组] [解决方案:遍历 + 哈希] [2021/11/15] [中等]
说明:链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。
分类:单向链表、双向链表、循环链表
优点:新增或删除慢,时间复杂度 O(1)
缺点:查找慢,时间复杂度 O(n)
案例:
- NC33 合并两个排序的链表 [链表] [解决方案:遍历] [2021/10/25] [简单]
- NC78 反转链表 [链表] [解决方案:遍历 + 三个指针] [2021/11/9] [简单]
- 25.K 个一组翻转链表 [链表] [2021/11/9] [困难]
- NC3 链表中环的入口结点 [链表] [解决方案:快慢指针 | 哈希] [2021/11/9] [简单]
- JZ6 从尾到头打印链表 [链表] [解决方案:哈希] [2021/11/11] [简单]
- JZ18 删除链表的节点 [链表] [2021/11/13] [简单]
说明:栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据结点的插入和删除操作。即“后进先出”的结构,我们称为Last InFirst Out,简称LIFO。
应用场景:深度优先搜索
案例:
- JZ30 包含min函数的栈 [剑指Offer | 栈] [2021/11/13] [简单]
说明:队列和栈类似,也是一种特殊的线性表。和栈不同的是,队列只允许在表的一端进行插入操作,而在另一端进行删除操作。即“先进先出”的结构,我们称为FirstIn First Out,简称FIFO。
应用场景:广度优先搜索
说明:散列表源自于散列函数(Hash function),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。
哈希表存储的是由键(key)和值(value)组成的数据。在哈希表中,我们可以利用哈希函数快速访问到数组中的目标数据。如果发生哈希冲突,就使用链表进行存储。这样一来,不管数据量为多少,我们都能够灵活应对。如果数组的空间太小,使用哈希表的时候就容易发生冲突,线性查找的使用频率也会更高;反过来,如果数组的空间太大,就会出现很多空箱子,造成内存的浪费。因此,给数组设定合适的空间非常重要。
在存储数据的过程中,如果发生冲突,可以利用链表在已有数据的后面插入新数据来解决冲突。这种方法被称为“链地址法”。
查找、插入、删除的时间复杂度都为 O(1),但是空间复杂度为O(n)
举例
- HJ23 删除字符串中出现次数最少的字符 [字符串] [解决方法:哈希表 + 遍历] [2021/10/27] [中等]
-
说明
堆是一种图的树形结构,被用于实现“优先队列”(priority queues)。优先队列是一种数据结构,可以自由添加数据,但取出数据时要从最小值开始按顺序取出。在堆的树形结构中,各个顶点被称为“结点”(node),数据就存储在这些结点中。
堆中的每个结点最多有两个子结点。树的形状取决于数据的个数。另外,结点的排列顺序为从上到下,同一行里则为从左到右。
堆分为最大堆和最小堆,最大堆的根节点是最大元素,最小堆的根节点是最小元素。在堆中存储数据时必须遵守这样一条规则:子结点必定小于/大于父结点。因此,最小值被存储在顶端的根结点中。往堆中添加数据时,为了遵守这条规则,一般会把新数据放在最下面一行靠左的位置。当最下面一行里没有多余空间时,就再往下另起一行,把数据加在这一行的最左端。
-
添加和取出操作
-
时间复杂度
堆中最顶端的数据始终最小,所以无论数据量有多少,取出最小值的时间复杂度都为O(1)。另外,因为取出数据后需要将最后的数据移到最顶端,然后一边比较它与子结点数据的大小,一边往下移动,所以取出数据需要的运行时间和树的高度成正比。假设数据量为n,根据堆的形状特点可知树的高度为log2n,那么重构树的时间复杂度便为O(logn)。添加数据也一样。在堆的最后添加数据后,数据会一边比较它与父结点数据的大小,一边往上移动,直到满足堆的条件为止,所以添加数据需要的运行时间与树的高度成正比,也是O(logn)。
-
用途
如果需要频繁地从管理的数据中取出最大值或最小值,那么使用堆来操作会非常方便。
-
说明
-
树是典型的非线性结构,它是包括,2 个结点的有穷集合 K。
-
二叉查找树/二叉搜索树
-
有两个特征:第一个是每个结点的值均大于其左子树上任意一个结点的值;第二个是每个结点的值均小于其右子树上任意一个结点的值。
-
结论:二叉查找树的最小结点要从顶端开始,往其左下的末端寻找。二叉查找树的最大结点要从顶端开始,往其右下的末端寻找。
-
添加和删除操作
添加:
删除:删除的结点只有一个子点,那么先删掉目标结点,然后把子结点移到被删除结点的位置上即可。如果需要删除的结点有两个子结点,那么先删掉目标结点,然后在被删除结点的左子树中寻找最大结点,最后将最大结点移到被删除结点的位置上。
-
时间复杂度
我们可以把二叉查找树当作是二分查找算法思想的树形结构体现(二分查找的详细说明在3-2节)。因为它具有前面提到的那两个性质,所以在查找数据或寻找适合添加数据的位置时,只要将其和现有的数据比较大小,就可以根据比较结果得知该往哪边移动了。比较的次数取决于树的高度。所以如果结点数为n,而且树的形状又较为均衡的话,比较大小和移动的次数最多就是log2n。因此,时间复杂度为O(logn)。但是,如果树的形状朝单侧纵向延伸,树就会变得很高,此时时间复杂度也就变成了O(n)。
-
-
平衡二叉查找树
-
是一种结构平衡的二叉搜索树,即叶节点高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。又有AVL树和红黑树。
-
AVL树
-
红黑树
红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
-
-
完全二叉树
若设二叉树的深度为k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,第k 层所有的结点都连续集中在最左边,这就是完全二叉树。
-
B树
-
-
二叉树遍历
-
遍历方式
前序遍历:根结点 ---> 左子树 ---> 右子树
中序遍历:左子树---> 根结点 ---> 右子树
后序遍历:左子树 ---> 右子树 ---> 根结点
层次遍历:只需按层次遍历即可 ,参考:BFS 的使用场景总结:层序遍历、最短路径问题)
-
递归遍历二叉树
前序遍历的递推公式: preOrder(r) = print r ---> preOrder(r->left) ---> preOrder(r->right)
中序遍历的递推公式: inOrder(r) = inOrder(r--->left) ---> print r ---> inOrder(r->right)
后序遍历的递推公式: postOrder(r) = postOrder(r->left) ---> postOrder(r->right) --->print r
-
二叉树递归和非递归遍历方法: Java 实现的二叉树的递归、非递归遍历
-
-
两种搜索方法
-
DFS
-
BFS
-
-
案例
- NC45 实现二叉树先序,中序和后序遍历 [2021/9/17] [中等]
- NC12 重建二叉树 [二叉树] [解决方案递归,利用前序查根节点,利用中序查左右子节点的坐标] [2021/9/17] [中等]
- NC14 按之字形顺序打印二叉树 [二叉树] [BFS + 层序遍历|队列] [2021/9/20] [简单]
- NC16 判断二叉树是否对称 [二叉树] [BFS + 层序遍历] [2021/11/9 | 2021/9/20] [简单]
- NC13 二叉树的最大深度 [二叉树] [DFS] [2021/9/20] [简单]
- NC102 在二叉树中找到两个节点的最近公共祖先 [二叉树] [DFS + 哈希] [2021/9/20] [中等]
- NC62 平衡二叉树 [二叉树] [DFS + 双递归] [2021/9/20] [中等]
- JZ54 二叉搜索树的第k个结点 [剑指Offer|二叉树] [中序遍历] [2021/11/11] [简单]
- JZ26 树的子结构 [剑指Offer|二叉树] [2021/11/13] [中等]
- JZ27 二叉树的镜像 [剑指Offer | 二叉树] [解决方案:递归] [2021/11/14] [简单]
说明:图是另一种非线性数据结构。在图结构中,数据结点一般称为顶点,而边是顶点的有序偶对。
加权图
有向图
-
时间复杂度
时间复杂度是一个可以描述运算时间的函数,常用大O符号来表达。O这个符号的意思是“忽略重要项以外的内容”,读音同Order。O(n2)的含义就是“算法的运行时间最长也就是n2的常数倍”。
冒泡排序就是重复“从序列右边开始比较相邻两个数字的大小,再根据结果交换两个数字的位置”这一操作的算法。在这个过程中,数字会像泡泡一样,慢慢从右往左“浮”到序列的顶端,所以这个算法才被称为“冒泡排序”。
在冒泡排序中,第1轮需要比较n-1次,第2轮需要比较n-2次……第n-1轮需要比较1次。因此,总的比较次数为(n-1)+(n-2)+…+1≈n2/2。这个比较次数恒定为该数值,和输入数据的排列顺序无关。不过,交换数字的次数和输入数据的排列顺序有关。假设出现某种极端情况,如输入数据正好以从小到大的顺序排列,那么便不需要任何交换操作;反过来,输入数据要是以从大到小的顺序排列,那么每次比较数字后便都要进行交换。因此,冒泡排序的时间复杂度为O(n2)。
package com.fufu.algorithm.sort;
import java.util.Arrays;
/**
* 冒泡排序
* Created by zhoujunfu on 2018/8/2.
*/
public class BubbleSort {
public static void sort(int[] array) {
if (array == null || array.length == 0) {
return;
}
int length = array.length;
//外层:需要length-1次循环比较
for (int i = 0; i < length - 1; i++) {
//内层:每次循环需要两两比较的次数,每次比较后,都会将当前最大的数放到最后位置,所以每次比较次数递减一次
for (int j = 0; j < length - 1 - i; j++) {
if (array[j] > array[j+1]) {
//交换数组array的j和j+1位置的数据
swap(array, j, j+1);
}
}
}
}
/**
* 交换数组array的i和j位置的数据
* @param array 数组
* @param i 下标i
* @param j 下标j
*/
public static void swap(int[] array, int i, int j) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
选择排序就是重复“从待排序的数据中寻找最小值,将其与序列最左边的数字进行交换”这一操作的算法。在序列中寻找最小值时使用的是线性查找。
选择排序使用了线性查找来寻找最小值,因此在第1轮中需要比较n-1个数字,第2轮需要比较n-2个数字……到第n-1轮的时候就只需比较1个数字了。因此,总的比较次数与冒泡排序的相同,都是(n-1)+(n-2)+…+1≈n2/2次。每轮中交换数字的次数最多为1次。如果输入数据就是按从小到大的顺序排列的,便不需要进行任何交换。选择排序的时间复杂度也和冒泡排序的一样,都为O(n2)。
public class SelectSort {
public static void sort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;
for (int j = i+1; j < arr.length; j ++) { //选出之后待排序中值最小的位置
if (arr[j] < arr[min]) {
min = j;
}
}
if (min != i) {
arr[min] = arr[i] + arr[min];
arr[i] = arr[min] - arr[i];
arr[min] = arr[min] - arr[i];
}
}
}
插入排序是一种从序列左端开始依次对数据进行排序的算法。在排序过程中,左侧的数据陆续归位,而右侧留下的就是还未被排序的数据。插入排序的思路就是从右侧的未排序区域内取出一个数据,然后将它插入到已排序区域内合适的位置上。
在插入排序中,需要将取出的数据与其左边的数字进行比较。就跟前面讲的步骤一样,如果左边的数字更小,就不需要继续比较,本轮操作到此结束,自然也不需要交换数字的位置。然而,如果取出的数字比左边已归位的数字都要小,就必须不停地比较大小,交换数字,直到它到达整个序列的最左边为止。具体来说,就是第k轮需要比较k-1次。因此,在最糟糕的情况下,第2轮需要操作1次,第3轮操作2次……第n轮操作n-1次,所以时间复杂度和冒泡排序的一样,都为O(n2)。和前面讲的排序算法一样,输入数据按从大到小的顺序排列时就是最糟糕的情况。
移位法:
public static void sort(int[] a) {
if (a == null || a.length == 0) {
return;
}
for (int i = 1; i < a.length; i++) {
int j = i - 1;
int temp = a[i]; // 先取出待插入数据保存,因为向后移位过程中会把覆盖掉待插入数
while (j >= 0 && a[j] > temp) { // 如果待是比待插入数据大,就后移
a[j+1] = a[j];
j--;
}
a[j+1] = temp; // 找到比待插入数据小的位置,将待插入数据插入
}
}
交换法:
public static void sort2(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
for (int i = 1; i < arr.length; i ++) {
int j = i - 1;
while (j >= 0 && arr[j] > arr[i]) {
arr[j + 1] = arr[j] + arr[j+1]; //只要大就交换操作
arr[j] = arr[j + 1] - arr[j];
arr[j + 1] = arr[j + 1] - arr[j];
System.out.println("Sorting: " + Arrays.toString(arr));
}
}
}
堆排序的特点是利用了数据结构中的堆。从降序排列的堆中取出数据时会从最大的数据开始取,所以将取出的数据反序输出,排序就完成了。
堆排序一开始需要将n个数据存进堆里,所需时间为O(nlogn)。排序过程中,堆从空堆的状态开始,逐渐被数据填满。由于堆的高度小于log2n,所以插入1个数据所需要的时间为O(logn)。每轮取出最大的数据并重构堆所需要的时间为O(logn)。由于总共有n轮,所以重构后排序的时间也是O(nlogn)。因此,整体来看堆排序的时间复杂度为O(nlogn)。这样来看,堆排序的运行时间比之前讲到的冒泡排序、选择排序、插入排序的时间O(n2)都要短,但由于要使用堆这个相对复杂的数据结构,所以实现起来也较为困难。
// 堆排序(最小堆)
public static void sort(int[] array) {
if (array == null || array.length == 0) {
return;
}
PriorityQueue<Integer> queue = new PriorityQueue<>(array.length);
for(int i : array) {
queue.offer(i);
}
int i = 0;
while (queue.size() != 0) {
array[i++] = queue.poll();
}
}
归并排序算法会把序列分成长度相同的两个子序列,当无法继续往下分时(也就是每个子序列中只有一个数据时),就对子序列进行归并。归并指的是把两个排好序的子序列合并成一个有序序列。
归并排序中,分割序列所花费的时间不算在运行时间内(可以当作序列本来就是分割好的)。在合并两个已排好序的子序列时,只需重复比较首位数据的大小,然后移动较小的数据,因此只需花费和两个子序列的长度相应的运行时间。也就是说,完成一行归并所需的运行时间取决于这一行的数据量。看一下上面的图便能得知,无论哪一行都是n个数据,所以每行的运行时间都为O(n)。而将长度为n的序列对半分割直到只有一个数据为止时,可以分成log2n行,因此,总共有log2n行。也就是说,总的运行时间为O(nlogn),这与前面讲到的堆排序相同。
package com.fufu.algorithm.sort;
import java.util.Arrays;
/**
* Created by zhoujunfu on 2018/8/10.
*/
public class MergeSort {
public static int[] sort(int [] a) {
if (a.length <= 1) {
return a;
}
int num = a.length >> 1;
int[] left = Arrays.copyOfRange(a, 0, num);
int[] right = Arrays.copyOfRange(a, num, a.length);
return mergeTwoArray(sort(left), sort(right));
}
public static int[] mergeTwoArray(int[] a, int[] b) {
int i = 0, j = 0, k = 0;
int[] result = new int[a.length + b.length]; // 申请额外空间保存归并之后数据
while (i < a.length && j < b.length) { //选取两个序列中的较小值放入新数组
if (a[i] <= b[j]) {
result[k++] = a[i++];
} else {
result[k++] = b[j++];
}
}
while (i < a.length) { //序列a中多余的元素移入新数组
result[k++] = a[i++];
}
while (j < b.length) {//序列b中多余的元素移入新数组
result[k++] = b[j++];
}
return result;
}
public static void main(String[] args) {
int[] b = {3, 1, 5, 4};
System.out.println(Arrays.toString(sort(b)));
}
}
快速排序算法首先会在序列中随机选择一个基准值(pivot),然后将除了基准值以外的数分为“比基准值小的数”和“比基准值大的数”这两个类别,再将其排列成以下形式。[比基准值小的数] 基准值 [比基准值大的数]
实现方式:填坑法、交换法、顺序遍历法
填坑法
/**
* 快速排序(挖坑法递归)
* @param arr 待排序数组
* @param low 左边界
* @param high 右边界
*/
public static void sort(int arr[], int low, int high) {
if (arr == null || arr.length <= 0) {
return;
}
if (low >= high) {
return;
}
int left = low;
int right = high;
int temp = arr[left]; //挖坑1:保存基准的值
while (left < right) {
while (left < right && arr[right] >= temp) {
right--;
}
arr[left] = arr[right]; //坑2:从后向前找到比基准小的元素,插入到基准位置坑1中
while (left < right && arr[left] <= temp) {
left ++;
}
arr[right] = arr[left]; //坑3:从前往后找到比基准大的元素,放到刚才挖的坑2中
}
arr[left] = temp; //基准值填补到坑3中,准备分治递归快排
System.out.println("Sorting: " + Arrays.toString(arr));
sort(arr, low, left-1);
sort(arr, left + 1, high);
}
交换法
/**
* 快速排序
* Created by zhoujunfu on 2018/8/6.
*/
public class QuickSort {
/**
* 快速排序(左右指针法)
* @param arr 待排序数组
* @param low 左边界
* @param high 右边界
*/
public static void sort2(int arr[], int low, int high) {
if (arr == null || arr.length <= 0) {
return;
}
if (low >= high) {
return;
}
int left = low;
int right = high;
int key = arr[left];
while (left < right) {
while (left < right && arr[right] >= key) {
right--;
}
while (left < right && arr[left] <= key) {
left++;
}
if (left < right) {
swap(arr, left, right);
}
}
swap(arr, low, left);
System.out.println("Sorting: " + Arrays.toString(arr));
sort2(arr, low, left - 1);
sort2(arr, left + 1, high);
}
public static void swap(int arr[], int low, int high) {
int tmp = arr[low];
arr[low] = arr[high];
arr[high] = tmp;
}
}
顺序遍历法
// 顺序遍历法
public static void sort3(int[] array, int low, int high) {
if(array == null && array.length == 1) {
return;
}
if(low >= high) {
return;
}
int povit = array[high];
int left = low , right = low;
while (right < high) {
while (right < high && array[right] >= povit) {
right++;
}
if (right < high) {
SwapUtil.swapByTmp(array, left, right);
left++;
}
}
SwapUtil.swapByTmp(array, left, high);
sort3(array, low, left - 1);
sort3(array, left + 1, high);
}
线性查找是一种在数组中查找数据的算法。线性查找的操作很简单,只要在数组中从头开始依次往下查找即可。若数据量为n,线性查找的时间复杂度便为O(n)。
二分查找通过比较数组中间的数据与目标数据的大小,可以得知目标数据是在数组的左边还是右边。
二分查找利用已排好序的数组,每一次查找都可以将查找范围减半。查找范围内只剩一个数据时查找结束。数据量为n的数组,将其长度减半log2n次后,其中便只剩一个数据了。也就是说,在二分查找中重复执行“将目标数据和数组中间的数据进行比较后将查找范围减半”的操作log2n次后,就能找到目标数据(若没找到则可以得出数据不存在的结论),因此它的时间复杂度为O(logn)。
// 二分查找(递归)
private static int search(int key, int[] array, int low, int high) {
if (low > high) {
return -1;
}
int mid = low + (high - low) / 2;
if (array[mid] == key) {
return mid;
} else {
if (array[mid] > key) {
return search(key, array, low, mid - 1);
} else {
return search(key, array, mid + 1, high);
}
}
}
- 概念:本质是搜索算法,指的是从问题所有可能的解的集合中一一枚举各元素。
- 优点:算法简单,在局部地方使用枚举法,效果十分的好
- 缺点:运算量过大,当问题的规模变大的时候,循环的阶数越大,执行速度越慢
-
概念:用一句话来形容递归算法的实现,就是在函数或者子过程的内部,直接或间接的调用自己算法。递归算法实际上是把问题转化成规模更小的同类子问题,先解决子问题,再通过相同的求解过程逐步解决更高层次的问题,最终获得最终的解。
-
三个要素:
递归边界:算法要有一个边界出口,能结束程序
参数收敛:每次调用参数都是收敛于递归边界
自身调用
-
概念:将原问题分成n个规模较小而结构与原问题相似的子问题。递归地对这些子问题求解,然后合并其结果就得到原问题的解。n=2时的分治法又称为二分法。
-
步骤
分解——将原问题分解成一系列子问题 解决——递归地解各子问题
合并——将子问题的结果合并成原问题的解
-
案例
- NC88 寻找第K大的值 [数组] [解决方案:基于快速排序的选择方法,分治] [2021/9/21] [中等]
- NC32 求平方根 [算术] [解决方案:二分法] [2021/9/22] [简单]
-
概念
贪心算法,又名贪婪法,是寻找最优解问题的常用方法,这种方法模式一般将求解过程分成若干个步骤,但每个步骤都应用贪心原则,选取当前状态下最好/最优的选择(局部最有利的选择),并以此希望最后堆叠出的结果也是最好/最优的解。贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。
-
步骤
步骤1:从某个初始解出发; 步骤2:采用迭代的过程,当可以向目标前进一步时,就根据局部最优策略,得到一部分解,缩小问题规模; 步骤3:将所有解综合起来。
-
案例
- 55.跳跃游戏 [数组] [解决方案:贪心算法] [2021/7/27,2021/10/12] [中等] - https://leetcode-cn.com/problems/jump-game/
- JZ67 剪绳子 [] [解决方案:贪心算法,每段长度3最大] [2021/10/12] [中等] - https://www.nowcoder.com/practice/57d85990ba5b440ab888fc72b0751bf8?tpId=13&tqId=587690&ru=/ta/sql-quick-study&qru=/ta/sql-quick-study/question-ranking
-
概念:滑动窗口算法在一个特定大小的字符串或数组上进行操作,而不在整个字符串和数组上操作,这样就降低了问题的复杂度,从而也达到降低了循环的嵌套深度。其实这里就可以看出来滑动窗口主要应用在数组和字符串上。
-
案例
- 3.无重复字符的最长子串 [字符串|集合] [解决方法:滑动窗口] [2021/7/7] - https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/
- 438.找到字符串中所有字母异位词 [字符串] [解决方案:滑动窗口 + 双指针] [2021/7/24] [2021/9/15] [中等] - https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/
- 76.最小覆盖子串 [字符串] [解决方案:滑动窗口] [2021/7/30] [困难] - https://leetcode-cn.com/problems/minimum-window-substring/
- 239.滑动窗口最大值 [数组 | 滑动窗口] [解决方案:优先队列,堆] [2021/7/31] [困难] - https://leetcode-cn.com/problems/sliding-window-maximum/
- WC45 最小覆盖子串 [字符串] [解决方案:滑动窗口] [2021/9/15] [困难] - https://www.nowcoder.com/practice/c466d480d20c4c7c9d322d12ca7955ac?tpId=202&&tqId=38754&rp=1&ru=/activity/oj&qru=/ta/code-written-high/question-ranking
-
概念:双指针,指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
-
用途
- 快慢指针
- 判定链表中是否含有环
- 已知链表中含有环,返回这个环的起始位置
- 寻找链表的中点
- 寻找链表的倒数第 k 个元素
- 左右指针
- 二分查找
- 两数之和
- 反转数组
- 滑动窗口算法
- 快慢指针
-
案例
- 75.颜色分类 [数组] [解决方案:双指针,类似快速排序 | API | 新建一个数组] [2021/7/22] [中等] - https://leetcode-cn.com/problems/sort-colors/
- 438.找到字符串中所有字母异位词 [字符串] [解决方案:滑动窗口 + 双指针] [2021/7/24] [中等] - https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/
- 11.盛最多水的容器 [数组,面积] [解决方案:双指针(左右)] [2021/8/3 2021/9/13] [中等] - https://leetcode-cn.com/problems/container-with-most-water/
- 142.环形链表 II [链表] [解决方案:哈希表 | 快慢指针,计算公式] [2021/7/22] [中等] - https://leetcode-cn.com/problems/linked-list-cycle-ii/
- NC53 删除链表的倒数第n个节点 [链表] [解决方案:双指针 | 栈] [2021/7/10] [中等] - https://www.nowcoder.com/practice/f95dcdafbde44b22a6d741baf71653f6?tpId=188&&tqId=38587&rp=1&ru=/activity/oj&qru=/ta/job-code-high-week/question-ranking
- NC41 最长无重复子数组 [字符串|集合] [解决方法:滑动窗口] [2021/9/13] [中等] - https://www.nowcoder.com/practice/b56799ebfd684fb394bd315e89324fb4?tpId=188&&tqId=38553&rp=1&ru=/activity/oj&qru=/ta/job-code-high-week/question-ranking
- NC103 反转字符串 [2021/9/13] [简单] - https://www.nowcoder.com/practice/c3a6afee325e472386a1c4eb1ef987f3?tpId=188&&tqId=38605&rp=1&ru=/activity/oj&qru=/ta/job-code-high-week/question-ranking
- NC54 数组中相加和为0的三元组 [数组] [2021/9/13] [中等] - https://www.nowcoder.com/practice/345e2ed5f81d4017bbb8cc6055b0b711?tpId=188&&tqId=38621&rp=1&ru=/activity/oj&qru=/ta/job-code-high-week/question-ranking
-
概念
-
用途
- 寻找下一个更大元素
- 寻找前一个更大元素
- 寻找下一个更小元素
- 寻找前一个更小元素
- qualified 的 窗口的 max/min
- sliding window max/min
-
模板
for 元素 in 列表: while 栈不为空 and 栈顶元素(大于或者小于)目标值: 出栈 根据业务处理 入栈
-
案例
-
- 下一个更大元素 I [数组] [解决方案:单调栈 ] [2021/9/9] [中等] - https://leetcode-cn.com/problems/next-greater-element-i/
-
503.下一个更大元素 II [数组] [解决方案:单调栈 ] [2021/9/10] [中等] - https://leetcode-cn.com/problems/next-greater-element-ii/
-
739.每日温度 [数组] [解决方案:暴力 | 单调栈 | 动态规划 ] [2021/9/11] [中等] - https://leetcode-cn.com/problems/daily-temperatures/
-
84.柱状图中最大的矩形[数组|矩阵] [解决方案:单调栈 ] [2021/7/26] [困难] - https://leetcode-cn.com/problems/largest-rectangle-in-histogram/
-
42.接雨水 [数组] [解决方案:方案一,动态规划,空间换时间。还有两种方法没有实践] [2021/7/20] [困难] - https://leetcode-cn.com/problems/trapping-rain-water/
-
-
思想
- 动态规划的核心思想就是拆分子问题,记住过往,减少重复计算
- 一个规模比较大的问题(可以用两三个参数表示的问题),可以通过若干规模较小的问题的结果来得到的(通常会寻求到一些特殊的计算逻辑,如求最值等)
-
步骤
- 穷举分析
- 确定边界
- 找出规律,确定最优子结构
- 写出状态转移方程
-
用途
- 如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景。
-
参考 图解算法——动态规划系列
-
案例
- HJ61 放苹果 [数字] [解决方法:动态规划] [2021/10/27] [简单]
- NC91 最长递增子序列 [数组] [解决方法:动态规划 + 二分法] [2021/10/22 [困难]
- NC19 子数组的最大累加和问题 [数组] [解决方法:动态规划] [2021/9/22、2021/10/16] [简单]
- NC127 最长公共子串 [数组] [解决方案:动态规划] [2021/10/16 | 2021/11/15] [中等]
- 剑指 Offer 47. 礼物的最大价值 [数组|矩阵] [解决方案:动态规划] [2021/9/6] [中等] - https://leetcode-cn.com/problems/li-wu-de-zui-da-jie-zhi-lcof/
- 64.最小路径和 [数组|矩阵] [解决方案:动态规划,3种状态转换] [2021/7/28] [中等] - https://leetcode-cn.com/problems/minimum-path-sum/
- 5.最长回文子串 [字符串] [动态规划 | 中心扩散] [2021/7/12] [中等] - https://leetcode-cn.com/problems/longest-palindromic-substring/
- 53.最大子序和 [数组] [解决方法:动态规划] [2021/7/14] [简单] - https://leetcode-cn.com/problems/maximum-subarray/
- 300.最长递增子序列 [数组] [解决方案:动态规划] [2021/7/19] [中等] - https://leetcode-cn.com/problems/longest-increasing-subsequence/
-
概念
回溯法(back tracking)(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
-
用途
可用于解决寻找所有可行解的题 ;
-
模板
result = [] def backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 in 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择
其核心就是 for 循环里面的递归,在递归调用之前「做选择」,在递归调用之后「撤销选择」,特别简单。
- 案例
- 46.全排列 [数组] [解决方案:回溯和深度优先搜索] [2021/7/20 、2021/10/11] - https://leetcode-cn.com/problems/permutations/ (重点参考:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/)
- 39.组合总和 [数组] [解决方案:搜索回溯] [2021/7/24 、 2021/10/12] - https://leetcode-cn.com/problems/combination-sum/
- 437. 路径总和 III [二叉树] [解决方案:前序遍历 | 前缀和、递归、回溯(注意0的路径)] [2021/7/26] [中等] - https://leetcode-cn.com/problems/path-sum-iii/
- 79.单词搜索 [二维数组] [解决方案:回溯] [2021/8/2] [中等] - https://leetcode-cn.com/problems/word-search/
- 剑指 Offer 12. 矩阵中的路径 [二维数组] [解决方案:回溯] [2021/8/2、2021/10/12] [中等] - https://leetcode-cn.com/problems/ju-zhen-zhong-de-lu-jing-lcof/ (同:79.单词搜索 - https://leetcode-cn.com/problems/word-search/)
- 494.目标和 [数组] [解决方案:回溯 + 暴力法] [2021/8/3] [中等] - https://leetcode-cn.com/problems/target-sum/
- 17.电话号码的字母组合 [数组|字符串] [解决方案:回溯] [2021/8/6] [中等] - https://leetcode-cn.com/problems/letter-combinations-of-a-phone-number/
- 参考 回溯算法详解修订版
-
BFS (广度优先遍历/搜索)Breadth First Search
-
概念
-
用途
- 层序遍历
- 最短路径 (在一棵树中,一个结点到另一个结点的路径是唯一的,但在图中,结点之间可能有多条路径,其中哪条路最近呢?这一类问题称为最短路径问题)
-
实现
广度优先搜索使用队列(queue)来实现,整个过程也可以看做一个倒立的树形:
- 把根节点放到队列的末尾。
- 每次从队列的头部取出一个元素,查看这个元素所有的下一级元素,把它们放到队列的末尾。并把这个元素记为它下一级元素的前驱。
- 找到所要找的元素时结束程序。
- 如果遍历整个树还没有找到,结束程序。
-
案例
- 200.岛屿数量 [][][数组] [解决方案:深度优先搜索 | 广度优先搜索] [2021/7/19] [中等] - https://leetcode-cn.com/problems/number-of-islands/
-
-
DFS (深度优先遍历/搜索)
-
概念
-
用途
-
实现
深度优先搜索用栈(stack)来实现,整个过程可以想象成一个倒立的树形:
- 把根节点压入栈中。
- 每次从栈中弹出一个元素,搜索所有在它下一级的元素,把这些元素压入栈中。并把这个元素记为它下一级元素的前驱。
- 找到所要找的元素时结束程序。
- 如果遍历整个树还没有找到,结束程序。
-
案例
- 200.岛屿数量 [][][数组] [解决方案:深度优先搜索 | 广度优先搜索] [2021/7/19] [中等] - https://leetcode-cn.com/problems/number-of-islands/
- NC13 二叉树的最大深度 [2021/9/17] [中等]
-
-
思想
顾名思义就是以某一个位置为中心,向周围扩散,直到满足条件或到达边界。
-
案例
- 5.最长回文子串 [字符串] [动态规划] [中心扩散] [2021/7/12,2021/10/18] [中等] - https://leetcode-cn.com/problems/longest-palindromic-substring/
-
概念
常用于在一个文本串 S 内查找一个模式串 P 的出现位置。
-
案例
-
异或操作
异或(xor)是一个数学运算符。它应用于逻辑运算。异或的数学符号为“⊕”,计算机符号为“xor”。异或也叫半加运算,其运算法则相当于不带进位的二进制加法:二进制下用1表示真,0表示假,则异或的运算法则为:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同为0,异为1),这些法则与加法是相同的,只是不带进位,所以异或常被认作不进位加法。
运算定律
-
对数函数 (logab)
案例
- JZ15 二进制中1的个数 [2021/11/14] [简单]