1. 排序算法
1、快排(不稳定)
1、Hoare经典版本
private void quickSort(int[] nums, int l, int h) {
if (h <= l)
return;
int j = partition(nums, l, h);
quickSort(nums, l, j - 1);
quickSort(nums, j + 1, h);
}
private int partition(int[] nums, int l, int h) {
int key = l;
int left =l,right=h;
while (left < right){
//right先走,找小
while (left < right&&nums[right] >= nums[key]){
right--;
}
//left后走,找大
while (left < right&&nums[left] <= nums[key]){
left++;
}
//交换left和right的值
if (left < right){
swap(nums,left,right);
}
}
swap(nums, l, left);
return left;
}
2、其它版本
- 挖坑法
private int partition(int[] nums, int left, int right) {
int key = nums[left];//在最左边形成一个坑位
while (left < right){
//right向左,找小
while (left < right&&nums[right] >= key)
{
right--;
}
//填坑
nums[left] = nums[right];
//left向右,找大
while (left < right&&nums[left] <= key)
{
left++;
}
//填坑
nums[right] = nums[left];
}
//L和R的相遇点为left;
nums[left] = key;//将key抛入坑位
return left;
}
- 前后指针法
private int partition(int[] nums, int left, int right) {
int prev = left;
int cur = left + 1;
while (cur <= right)//当cur未越界时继续
{
if (nums[cur] < nums[left] && ++prev != cur)//cur指向的内容小于key
{
swap(nums,prev,cur);
}
cur++;
}
//prev即为划分的位置
swap(nums,left,prev);
return prev;
}
3. 非递归实现
快速排序的非递归算法基本思路:
1、先将待排序列的第一个元素的下标和最后一个元素的下标入栈。
2、当栈不为空时,读取栈中的信息(一次读取两个:一个是L,另一个是R),然后调用某一版本的单趟排序,排完后获得了key的下标,然后判断key的左序列和右序列是否还需要排序,若还需要排序,就将相应序列的L和R入栈;若不需排序了(序列只有一个元素或是不存在),就不需要将该序列的信息入栈。
3、反复执行步骤2,直到栈为空为止。
//快速排序(非递归实现)
void QuickSortNonR(int[] nums, int begin, int end)
{
Stack<Integer> st = new Stack<>();//创建栈
st.push(begin);//待排序列的L
st.push(end);//待排序列的R
while (!st.isEmpty())
{
int right = st.pop();//读取R
int left = st.pop();//读取L
//该处调用的是Hoare版本的单趟排序
int key = partition(nums, left, right);
if (left < key - 1)//该序列的左序列还需要排序
{
st.push(left);//左序列的L入栈
st.push(key - 1);//左序列的R入栈
}
if (key + 1 < right)// 该序列的右序列还需要排序
{
st.push(key + 1);//右序列的L入栈
st.push(right);//右序列的R入栈
}
}
}
4. 算法改进
- 切换到插入排序
因为快速排序在小数组中也会递归调用自己也不能避免随着递归的深入,每一层的递归次数会以2倍的形式快速增长。,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。
//优化后的快速排序
void QuickSort0(int[] nums, int begin, int end){
//当只有一个数据或是序列不存在时,不需要进行操作
if (begin >= end) return;
//可自行调整
if (end - begin + 1 > 20) {
//可调用快速排序的单趟排序三种中的任意一种
int key = partition(a, begin, end);
quickSort(a, begin, key - 1);
quickSort(a, key + 1, end);
}else{
//insertSort(a, end - begin + 1);
shellSort(a, end - begin + 1);
}
}
- 三数取中
当待排序列本就是一个有序的序列时,我们若是依然每次都选取最左边或是最右边的数作为key,那么快速排序的效率将达到最低,时间复杂度退化为O(N2)。对快速排序效率影响最大的就是选取的key,若选取的key越接近中间位置,则则效率越高。
但是计算中位数的代价很高。一种折中方法是取 3 个元素,并将大小居中的元素作为切分元素。
//三数取中
int getMidIndex(int[] nums, int left, int right){
int mid = left + (right - left) / 2;
if (nums[mid] > nums[left]){
if (nums[mid] < nums[right])
return mid;
else if (nums[left]>nums[right])
return left;
else
return right;
}else{
if (nums[mid] > nums[right])
return mid;
else if (nums[left] > nums[right])
return right;
else
return left;
}
}
//只需在单趟排序代码开头加上以下代码
int midIndex = getMidIndex(nums, begin, end); //获取大小居中的数的下标
swap(nums,begin, midIndex); //将该数与序列最左端的数据交换
- 三向切分
对于有大量重复元素的数组,可以将数组切分为三部分,分别对应小于、等于和大于切分元素。
三向切分快速排序对于有大量重复元素的随机数组可以在线性时间内完成排序。
void sort(T[] nums, int l, int h) {
if (h <= l) {
return;
}
int lt = l, i = l + 1, gt = h;
T v = nums[l];
while (i <= gt) {
int cmp = nums[i].compareTo(v);
if (cmp < 0) {
swap(nums, lt++, i++);
} else if (cmp > 0) {
swap(nums, i, gt--);
} else {
i++;
}
}
sort(nums, l, lt - 1);
sort(nums, gt + 1, h);
}
2、归并排序(稳定)
- 自顶向下归并排序
将一个大数组分成两个小数组去求解。
因为每次都将问题对半分成两个子问题,这种对半分的算法复杂度一般为 O(NlogN)。
public static void mergeSort(int[] data, int left, int right) {
if (left >= right)
return;
// 找出中间索引
int mid = (left + right) / 2;
// 对左边数组进行递归
mergeSort(data, left, mid);
// 对右边数组进行递归
mergeSort(data, mid + 1, right);
// 合并
merge(data, left, mid, right);
}
//将两个数组进行归并,归并前面2个数组已有序,归并后依然有序
public static void merge(int[] data, int left, int mid, int right) {
// 临时数组
int[] tmpArr = new int[data.length];
// 右数组第一个元素索引
int mid = mid + 1;
// third 记录临时数组的索引
int third = left;
// 缓存左数组第一个元素的索引
int tmp = left;
while (left <= mid && mid <= right) {
// 从两个数组中取出最小的放入临时数组
if (data[left] <= data[mid]) {
tmpArr[third++] = data[left++];
} else {
tmpArr[third++] = data[mid++];
}
}
// 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
while (mid <= right) {
tmpArr[third++] = data[mid++];
}
while (left <= mid) {
tmpArr[third++] = data[left++];
}
// 将临时数组中的内容拷贝回原数组中
// (原left-right范围的内容被复制回原数组)
while (tmp <= right) {
data[tmp] = tmpArr[tmp++];
}
- 自底向上归并排序
先归并那些微型数组,然后成对归并得到的微型数组。
public static void mergeSort(int[] data, int left, int right) {
int N = data.length;
for (int sz = 1; sz < N; sz += sz) {
for (int lo = 0; lo < N - sz; lo += sz + sz) {
merge(nums, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1));
}
}
}
- 外排序
外排序:数据量较大,内存中放不下,数据只能放到磁盘文件中。
//to do
3、堆排序(不稳定)
堆排序是一种原地排序,没有利用额外的空间。
现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。
- 向下调整算法
//使用堆的向下调整算法,最坏的情况下(即一直需要交换结点),需要循环的次数为:h - 1次(h为树的高度)。
//而h = log2(N+1)(N为树的总结点数)。所以堆的向下调整算法的时间复杂度为:O(logN) 。
public void adjustHeap(int[] array, int i) {
int maxIndex = i;
//如果有左子树,且左子树大于父节点,则将最大指针指向左子树
if (i * 2 < len && array[i * 2] > array[maxIndex])
maxIndex = i * 2 + 1;
//如果有右子树,且右子树大于父节点,则将最大指针指向右子树
if (i * 2 + 1 < len && array[i * 2 + 1] > array[maxIndex])
maxIndex = i * 2 + 2;
//如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。
if (maxIndex != i) {
swap(array, maxIndex, i);
adjustHeap(array, maxIndex);
}
}
- 建堆
// 从倒数第一个非叶子结点开始,从后往前,按下标,依次作为根去向下调整建堆。
// 建堆的时间复杂度: T(n)=O(N)。
public void buildMaxHeap(int[] array) {
//从最后一个非叶子节点开始向上构造最大堆
//for循环这样写会更好一点:i的左子树和右子树分别2i+1和2(i+1)
for (int i = (len/2- 1); i >= 0; i--) {
adjustHeap(array, i);
}
}
- 排序
//将堆顶数据与堆的最后一个数据交换,然后对根位置进行一次堆的向下调整,但是调整时被交换到最后的那个最大的数不参与向下调整。
//然后又将堆顶数据与堆的最后一个数据交换,反复执行下去,直到堆中只有一个数据时便结束。此时该序列就是一个升序。
public int[] HeapSort(int[] array) {
len = array.length;
if (len < 1) return array;
//1.构建一个最大堆
buildMaxHeap(array);
//2.循环将堆首位(最大值)与末位交换,然后在重新调整最大堆
while (len > 0) {
swap(array, 0, len - 1);
len--;
adjustHeap(array, 0);
}
return array;
}
-
java优先队列
-
构造方法
PriorityQueue()//默认容量大小为11 this(DEFAULT_INITIAL_CAPACITY, null);
PriorityQueue(int initialCapacity) //指定初始容量
PriorityQueue(Collection c) //包含集合元素
PriorityQueue(int initialCapacity, Comparator comparator) //指定初始容量和比较器 -
方法
继承了Queue(3组方法)和Collection(6个方法)
public boolean add(E e); //在队尾插入元素,插入失败时抛出异常,并调整堆结构 public boolean offer(E e); //在队尾插入元素,插入失败时抛出false,并调整堆结构 public E remove(); //获取队头元素并删除,并返回,失败时前者抛出异常,再调整堆结构 public E poll(); //获取队头元素并删除,并返回,失败时前者抛出null,再调整堆结构 public E element(); //返回队头元素(不删除),失败时前者抛出异常 public E peek();//返回队头元素(不删除),失败时前者抛出null public boolean isEmpty(); //判断队列是否为空 public int size(); //获取队列中元素个数 public void clear(); //清空队列 public boolean contains(Object o); //判断队列中是否包含指定元素(从队头到队尾遍历) public Iterator<E> iterator(); //迭代器 public <T> T[] toArray(T[] a);
-
实现
- 插入
//在位置 k (调用offer,add方法时为末尾位置即 k = size) 处插入项 x,通过将 x 提升到树上直到它大于或等于其父项或者是根来保持堆不变。 private static <T> void siftUpComparable(int k, T x, Object[] es) { Comparable<? super T> key = (Comparable<? super T>) x; while (k > 0) { int parent = (k - 1) >>> 1; Object e = es[parent]; if (key.compareTo((T) e) >= 0) break; es[k] = e; k = parent; } es[k] = key; }
- 删除
//删除元素后,将队尾元素复制到队头,并从堆顶到堆底调整堆。 private static <T> void siftDownComparable(int k, T x, Object[] es, int n) { // assert n > 0; Comparable<? super T> key = (Comparable<? super T>)x; int half = n >>> 1; // loop while a non-leaf while (k < half) { int child = (k << 1) + 1; // assume left child is least Object c = es[child]; int right = child + 1; if (right < n && ((Comparable<? super T>) c).compareTo((T) es[right]) > 0) c = es[child = right]; if (key.compareTo((T) c) <= 0) break; es[k] = c; k = child; } es[k] = key; }
-
使用
- TopK问题
//时间复杂度:O(NlogK),遍历数据 O(N),堆内元素调整 O(K),空间复杂度:O(K) public static int findKthLargest(int[] nums, int k){ int len = nums.length; // 使用一个含有 k 个元素的最小堆 // k 堆的初始容量,(a,b) -> a -b 比较器 PriorityQueue<Integer> minTopK = new PriorityQueue<>(k,(a, b) -> a -b); for (int i = 0; i < k; i++){ minTopK.add(nums[i]); } for (int i = k; i < len; i++){ Integer topEle = minTopK.peek(); // 返回队头元素(不删除),失败时前者抛出null // 只要当前遍历的元素比堆顶元素大,堆顶弹出,遍历的元素进去 if (nums[i] > topEle){ minTopK.poll(); // 获取队头元素并删除,并返回,失败时前者抛出null,再调整堆结构 minTopK.offer(nums[i]); // 在队尾插入元素,插入失败时抛出false,并调整堆结构 } } return minTopK.peek(); }
-
4、三个O(n^2)算法及希尔排序
//冒泡排序(稳定)
public static int[] bubbleSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++)
for (int j = 0; j < array.length - 1 - i; j++)
if (array[j + 1] < array[j]) {
int temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
return array;
}
// 选择排序(不稳定)
public static int[] selectionSort(int[] array) {
if (array.length == 0)
return array;
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i; j < array.length; j++) {
if (array[j] < array[minIndex]) //找到最小的数
minIndex = j; //将最小数的索引保存
}
int temp = array[minIndex];
array[minIndex] = array[i];
array[i] = temp;
}
return array;
}
// 插入排序(稳定)
public static int[] insertionSort(int[] array) {
if (array.length == 0)
return array;
int current;
for (int i = 0; i < array.length - 1; i++) {
current = array[i + 1];
int preIndex = i;
while (preIndex >= 0 && current < array[preIndex]) {
array[preIndex + 1] = array[preIndex];
preIndex--;
}
array[preIndex + 1] = current;
}
return array;
//希尔排序(不稳定)
public static int[] ShellSort(int[] array) {
int len = array.length;
int temp, gap = len / 2;
while (gap > 0) {
for (int i = gap; i < len; i++) {
temp = array[i];
int preIndex = i - gap;
while (preIndex >= 0 && array[preIndex] > temp) {
array[preIndex + gap] = array[preIndex];
preIndex -= gap;
}
array[preIndex + gap] = temp;
}
gap /= 2;
}
return array;
}
5、基数排序和桶排序(计数排序)
//计数排序
//其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种稳定的线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
//使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。然后根据数组C来将A中的元素排到正确的位置。它只能对整数进行排序。
public static int[] CountingSort(int[] array) {
if (array.length == 0) return array;
int bias, min = array[0], max = array[0];
for (int i = 1; i < array.length; i++) {
if (array[i] > max)
max = array[i];
if (array[i] < min)
min = array[i];
}
bias = 0 - min;
int[] bucket = new int[max - min + 1];
for (int i = 0; i < array.length; i++) {
bucket[array[i] + bias]++;
}
int index = 0, i = 0;
while (index < array.length) {
if (bucket[i] != 0) {
array[index] = i - bias;
bucket[i]--;
index++;
} else
i++;
}
return array;
//桶排序
//将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)
//基数排序
//是非比较的排序算法,对每一位进行排序,从最低位开始排序,复杂度为O(kn),为数组长度,k为数组中的数的最大的位数;
//基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。
//有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
//基数排序基于分别排序,分别收集,所以是稳定的。
public static int[] RadixSort(int[] array) {
if (array == null || array.length < 2)
return array;
// 1.先算出最大数的位数;
int max = array[0];
for (int i = 1; i < array.length; i++) {
max = Math.max(max, array[i]);
}
int maxDigit = 0;
while (max != 0) {
max /= 10;
maxDigit++;
}
int mod = 10, div = 1;
ArrayList<ArrayList<Integer>> bucketList = new ArrayList<ArrayList<Integer>>();
for (int i = 0; i < 10; i++)
bucketList.add(new ArrayList<Integer>());
for (int i = 0; i < maxDigit; i++, mod *= 10, div *= 10) {
for (int j = 0; j < array.length; j++) {
int num = (array[j] % mod) / div;
bucketList.get(num).add(array[j]);
}
int index = 0;
for (int j = 0; j < bucketList.size(); j++) {
for (int k = 0; k < bucketList.get(j).size(); k++)
array[index++] = bucketList.get(j).get(k);
bucketList.get(j).clear();
}
}
return array;
}
2. 字符串
1、KMP
// KMP 算法
// ss: 原串(string) pp: 匹配串(pattern)
public int strStr(String ss, String pp) {
if (pp.isEmpty()) return 0;
// 分别读取原串和匹配串的长度
int n = ss.length(), m = pp.length();
// 原串和匹配串前面都加空格,使其下标从 1 开始
ss = " " + ss;
pp = " " + pp;
char[] s = ss.toCharArray();
char[] p = pp.toCharArray();
// 构建 next 数组,数组长度为匹配串的长度(next 数组是和匹配串相关的)
int[] next = new int[m + 1];
// 构造过程 i = 2,j = 0 开始,i 小于等于匹配串长度 【构造 i 从 2 开始】
for (int i = 2, j = 0; i <= m; i++) {
// 匹配不成功的话,j = next(j)
while (j > 0 && p[i] != p[j + 1]) j = next[j];
// 匹配成功的话,先让 j++
if (p[i] == p[j + 1]) j++;
// 更新 next[i],结束本次循环,i++
next[i] = j;
}
// 匹配过程,i = 1,j = 0 开始,i 小于等于原串长度 【匹配 i 从 1 开始】
for (int i = 1, j = 0; i <= n; i++) {
// 匹配不成功 j = next(j)
while (j > 0 && s[i] != p[j + 1]) j = next[j];
// 匹配成功的话,先让 j++,结束本次循环后 i++
if (s[i] == p[j + 1]) j++;
// 整一段匹配成功,直接返回下标
if (j == m) return i - m;
}
return -1;
}
2、回文串之中心扩展
//Lc5
class Solution {
public String longestPalindrome(String s) {
if (s == null || s.length() == 0) {
return "";
}
// 保存起始位置,测试了用数组似乎能比全局变量稍快一点
int[] range = new int[2];
char[] str = s.toCharArray();
for (int i = 0; i < s.length(); i++) {
// 把回文看成中间的部分全是同一字符,左右部分相对称
// 找到下一个与当前字符不同的字符
i = findLongest(str, i, range);
}
return s.substring(range[0], range[1] + 1);
}
public static int findLongest(char[] str, int low, int[] range) {
// 查找中间部分
int high = low;
while (high < str.length - 1 && str[high + 1] == str[low]) {
high++;
}
// 定位中间部分的最后一个字符
int ans = high;
// 从中间向左右扩散
while (low > 0 && high < str.length - 1 && str[low - 1] == str[high + 1]) {
low--;
high++;
}
// 记录最大长度
if (high - low > range[1] - range[0]) {
range[0] = low;
range[1] = high;
}
return ans;
}
}
3、回文串之Manacher
3. 查找
1、二分法
口诀
求左边界:向下取整,等号归右,左加一
求右边界:向上取整,等号归左,右减一
总是右侧为所求
//求左边界
int left = 0, right = n-1;
while(left < right){//求左边界(注意这里不要等号)
int mid = (left+right)>>1;//向下取整
if(nums[mid] >= target) right = mid;//等号归右
else left = mid+1;//左加一
}
//此时right即为所求
//lc 4寻找两个有序数组的中位数
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int n = nums1.length;
int m = nums2.length;
int left = (n + m + 1) / 2;
int right = (n + m + 2) / 2;
//将偶数和奇数的情况合并,如果是奇数,会求两次同样的 k 。
return (getKth(nums1, 0, n - 1, nums2, 0, m - 1, left) + getKth(nums1, 0, n - 1, nums2, 0, m - 1, right)) * 0.5;
}
private int getKth(int[] nums1, int start1, int end1, int[] nums2, int start2, int end2, int k) {
int len1 = end1 - start1 + 1;
int len2 = end2 - start2 + 1;
//让 len1 的长度小于 len2,这样就能保证如果有数组空了,一定是 len1
if (len1 > len2) return getKth(nums2, start2, end2, nums1, start1, end1, k);
if (len1 == 0) return nums2[start2 + k - 1];
if (k == 1) return Math.min(nums1[start1], nums2[start2]);
int i = start1 + Math.min(len1, k / 2) - 1;
int j = start2 + Math.min(len2, k / 2) - 1;
if (nums1[i] > nums2[j]) {
return getKth(nums1, start1, end1, nums2, j + 1, end2, k - (j - start2 + 1));
}
else {
return getKth(nums1, i + 1, end1, nums2, start2, end2, k - (i - start1 + 1));
}
}
1、旋转数组
//Lc81 搜索旋转数组二分查找
public boolean search(int[] nums, int target) {
int lo = 0, hi = nums.length - 1, mid = 0;
while (lo <= hi) {
mid = lo + (hi - lo) / 2;
if (nums[mid] == target) {
return true;
}
// 先根据 nums[mid] 与 nums[lo] 的关系判断 mid 是在左段还是右段
if (nums[mid] > nums[lo]) {
// 再判断 target 是在 mid 的左边还是右边,从而调整左右边界 lo 和 hi
if (target >= nums[lo] && target < nums[mid]) {
hi = mid - 1;
} else {
lo = mid + 1;
}
} else if(nums[mid] < nums[lo]) {
if (target > nums[mid] && target <= nums[hi]) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}else{
lo++;
}
}
return false;
}
//Lc154 寻找旋转排序数组中的最小值
public int findMin(int[] nums) {
int left =0,right =nums.length-1;
if(left==right||nums[left]<nums[right]) return nums[left];
while(left<right){
int mid = (right-left>>1)+left;
if(nums[mid]<nums[right]) right = mid;
else if(nums[mid]>nums[right]) left =mid+1;
//nums[mid]与nums[right],则right右移,相等有两种情况[2,2,1,2]、[1,2,2,2]
else right--;
}
return nums[right];
}
2、二分矩阵
//Lc378 有序矩阵中第 K 小的元素
//用优先队列进行归并排序
public int kthSmallest(int[][] matrix, int k) {
PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] a, int[] b) {
return a[0] - b[0];
}
});
int n = matrix.length;
for (int i = 0; i < n; i++) {
pq.offer(new int[]{matrix[i][0], i, 0});
}
for (int i = 0; i < k - 1; i++) {
int[] now = pq.poll();
if (now[2] != n - 1) {
pq.offer(new int[]{matrix[now[1]][now[2] + 1], now[1], now[2] + 1});
}
}
return pq.poll()[0];
}
//二分矩阵
public int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
int left = matrix[0][0];
int right = matrix[n - 1][n - 1];
while (left < right) {
int mid = left + ((right - left) >> 1);
if (check(matrix, mid, k, n)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
public boolean check(int[][] matrix, int mid, int k, int n) {
int i = n - 1;
int j = 0;
int num = 0;
while (i >= 0 && j < n) {
if (matrix[i][j] <= mid) {
num += i + 1;
j++;
} else {
i--;
}
}
return num >= k;
}
2、Top-K
1、快速选择算法
快速排序的 partition() 方法,会返回一个整数 j 使得 a[l…j-1] 小于等于 a[j],且 a[j+1…h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 k 个元素。
该算法是线性级别的,假设每次能将数组二分,那么比较的总次数为 (N+N/2+N/4+…),直到找到第 k 个元素,这个和显然小于 2N。
public T quickSelect(T[] nums, int k) {
int l = 0, h = nums.length - 1;
while (h > l) {
//快速排序的 partition() 方法
int j = partition(nums, l, h);
if (j == k) {
return nums[k];
} else if (j > k) {
h = j - 1;
} else {
l = j + 1;
}
}
return nums[k];
}
private int partition(int[] nums, int l, int h) {
if(l==h) return l;
int left =l,right=h;
while (left < right){
//right先走,找小
while (left < right&&nums[right] <= nums[l]){
right--;
}
//left后走,找大
while (left < right&&nums[left] >= nums[l]){
left++;
}
//交换left和right的值
if (left < right){
swap(nums,left,right);
}
}
swap(nums, l, left);
return left;
}
2、k大的堆
public int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> que = new PriorityQueue<>(k);
for(int i :nums){
if(que.size()<k) que.add(i);
else{
if(que.peek()<i){
que.poll();
que.offer(i);
}
}
}
return que.peek();
}
3、前缀和
//560
int[] sum =new int[nums.length+1];
int res =0;
Map<Integer,Integer> map =new HashMap<>();
for(int i= 1;i<nums.length+1;i++){
sum[i] =sum[i-1]+nums[i-1];
map.put(sum[i-1],map.getOrDefault(sum[i-1],0)+1);
res+=map.getOrDefault(sum[i]-k,0);
}
return res;
4. 双指针
1、头尾
2、滑动窗口
3、快慢指针
6. 贪心
1、基础规则数组贪心
// 135 相邻规则转化为左右规则,两次遍历
public int candy(int[] ratings) {
int n = ratings.length;
int[] left = new int[n];
for (int i = 0; i < n; i++) {
if (i > 0 && ratings[i] > ratings[i - 1]) {
left[i] = left[i - 1] + 1;
} else {
left[i] = 1;
}
}
int right = 0, ret = 0;
for (int i = n - 1; i >= 0; i--) {
if (i < n - 1 && ratings[i] > ratings[i + 1]) {
right++;
} else {
right = 1;
}
ret += Math.max(left[i], right);
}
return ret;
}
2、单调栈
-
操作规则(以单调递增栈为例)
- 如果新的元素比栈顶元素大,就入栈
- 如果新的元素较小,那就一直把栈内元素弹出来,直到栈顶比新元素小
-
效果
- 栈内的元素是递增的
- 当元素出栈时,说明这个新元素是出栈元素向后找第一个比其小的元素
- 当元素出栈后,说明新栈顶元素是出栈元素向前找第一个比其小的元素
-
一般模板
stack<Integer> st;
for(int i = 0; i < nums.size(); i++)
{
while(!st.isEmpty() && st.top() > nums[i])
{
st.pop();
}
st.push(nums[i]);
}
// 84
public int largestRectangleArea(int[] heights) {
int res = 0;
Deque<Integer> stack = new ArrayDeque<>();
int[] new_heights = new int[heights.length + 2];
for (int i = 1; i < heights.length + 1; i++) {
new_heights[i] = heights[i - 1];
}
for (int i = 0; i < new_heights.length; i++) {
while (!stack.isEmpty() && new_heights[stack.peek()] > new_heights[i]) {
int cur = stack.pop();
int l = stack.peek();
int r = i;
res = Math.max(res, (r - l - 1) * new_heights[cur]);
}
stack.push(i);
}
return res;
}
//左右两次遍历更好理解
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] left = new int[n];
int[] right = new int[n];
Deque<Integer> mono_stack = new ArrayDeque<Integer>();
for (int i = 0; i < n; ++i) {
while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
mono_stack.pop();
}
left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
mono_stack.push(i);
}
mono_stack.clear();
for (int i = n - 1; i >= 0; --i) {
while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
mono_stack.pop();
}
right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek());
mono_stack.push(i);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
}
return ans;
}
3、LIS问题
最优方法为贪心+二分
https://blog.csdn.net/George__Yu/article/details/75896330
class Solution {
public int lengthOfLIS(int[] nums) {
int[] tails = new int[nums.length];
int res = 0;
for(int num : nums) {
int i = 0, j = res;
while(i < j) {
int m = (i + j) / 2;
if(tails[m] < num) i = m + 1;
else j = m;
}
tails[i] = num;
if(res == j) res++;
}
return res;
}
}
//二维度 Lc面试题 17.08||354. 俄罗斯套娃信封问题
public int maxEnvelopes(int[][] envelopes) {
//关键:排序时相同宽度,按高度降序排序,便成了高度的LIS问题
Arrays.sort(envelopes,(o1, o2) -> o1[0]==o2[0]?o2[1]-o1[1]: o1[0]-o2[0]);
int len = envelopes.length, k =0;
int[] arr =new int[len+1];
arr[0] = -1;
for(int[] pair:envelopes){
if(pair[1]>arr[k]) arr[++k] =pair[1];
else{
//二分查找
int left =1,right =k;
while(left<right){
int mid = left+(right-left>>1);
if(arr[mid]>=pair[1]) right=mid;
else left=mid+1;
}
arr[right]=pair[1];
}
}
return k;
}
7. 树
1、遍历
/**
* 前序遍历
*/
public void preOrder(TreeNode node){
if(node != null){
visited(node);
preOrder(node.leftChild);
preOrder(node.rightChild);
}
}
/**
* 中序遍历
*/
public void inOrder(TreeNode node){
if(node != null){
preOrder(node.leftChild);
visited(node);
preOrder(node.rightChild);
}
}
/**
* 后序遍历
*/
public void postOrder(TreeNode node){
if(node != null){
preOrder(node.leftChild);
preOrder(node.rightChild);
visited(node);
}
}
/**
* 非递归前序遍历
*/
public void nonRecPreOrder(TreeNode node){
Stack<TreeNode> stack = new Stack<>();
TreeNode pNode = node;
while(pNode != null || stack.size()>0){
if(pNode != null){
visited(pNode);
stack.push(pNode);
pNode = pNode.leftChild;
}else(stack.size()>0){
pNode = stack.pop();
pNode = pNode.rightChild;
}
}
}
/**
* 非递归中序遍历
*/
public void nonRecInOrder(TreeNode node){
Stack<TreeNode> stack = new Stack<>();
TreeNode pNode = node;
while(pNode != null || stack.size()>0){
if(pNode != null){
stack.push(pNode);
pNode = pNode.leftChild;
}else{
pNode = stack.pop();
visited(pNode);
pNode = pNode.rightChild;
}
}
}
/**
* 非递归后序遍历
*/
public void nonRecPostOrder(TreeNode node){
Stack<TreeNode> stack = new Stack<>();
TreeNode pNode = node;
node =null; //记录上一个已输出的节点
while(pNode != null|| !stack.isEmpty()){
//左子树入栈
if(pNode!= null){
stack.push(pNode);
pNode = pNode.leftChild;
//当前节点无右子树或者右子树已输出
}else{
pNode =stack.peek();
if(pNode.rightChild!=null && node!=pNode.rightChild){
pNode=pNode.rightChild;
}else{
stack.pop();
visited(pNode);
//记录上一个已输出的节点
node = pNode;
//防止以访问的当前节点被压入堆栈,所以要置空
pNode=null;
}
}
}
}
/**
* 层序遍历(非递归)
*/
//带层高
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<List<Integer>>();
if (root == null) {
return res;
}
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
while (!queue.isEmpty()) {
List<Integer> level = new ArrayList<Integer>();
int currentLevelSize = queue.size();
for (int i = 1; i <= currentLevelSize; ++i) {
TreeNode node = queue.poll();
level.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
res.add(level);
}
return res;
}
/**
* 层序遍历(递归)
*/
//DFS实现层次遍历
public List<List<Integer>> levelOrder(Node root) {
List<List<Integer>> res =new ArrayList<>();
if (root==null) {
return res;
}
dfs(res,root,0);
return res;
}
private void dfs(List<List<Integer>> res,Node root,int lv){
if (res.size()<=lv) {
res.add(new ArrayList<Integer>());
}
res.get(lv).add(root.val);
if (root.children!=null) {
for (Node node : root.children) {
dfs(res,node,lv+1);
}
}
}
/**
* 计算树的高度
*/
private int height(TreeNode node){
if(node == null){
return 0;
}else{
int i = height(node.leftChild);
int j = height(node.rightChild);
return (i<j)?j+1:i+1;
}
}
/**
* 计算树的节点数
*/
private int size(TreeNode node){
if(node == null){
return 0;
}else{
return 1+size(node.leftChild)+size(node.rightChild);
}
}
//Lc236( 二叉树的最近公共祖先--后序)
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root == p || root == q){
return root;
}
TreeNode left = lowestCommonAncestor(root.left, p, q);
TreeNode right = lowestCommonAncestor(root.right, p, q);
if (left == null){
return right;
}
if (right == null){
return left;
}
return root;
}
// Lc114(展开二叉树--后序)
public void flatten(TreeNode root) {
if (root == null){
return;
}
flatten(root.left);
flatten(root.right);
TreeNode tem = root.right;
root.right = root.left;
root.left = null;
TreeNode t = root;
while(t.right != null){
t = t.right;
}
t.right = tem;
}
2、展开与重构
8. 图
1、遍历
2、回溯法
模板
void backtracking(参数){
if(终止条件){
收集结果;
return;
}
for(集合元素){
处理节点;
递归;
回溯;
}
return;
}
三部曲:
- 递归函数参数和返回值
- 确定终止条件
- 单层递归逻辑
- 组合
- 返回值设为void,设置全局变量二维数组result,一维数组路径path。参数集合长度n,结果长度k,起始数startindex
- path.size()==k(结果满足条件)
- 从startindex到n(剪枝startindex>n-k)开始遍历并递归
- 切割
- 子集
- 排列
- 棋盘
3、记忆化搜索
4、拓朴排序
BFS
- 统计入度indegree
- 入度为0则入队
- 出队时其指向的点入度减一,重复23
- 统计出度的点的数量是否等于总点数
DFS
借助一个标志列表 flags,用于判断每个节点 i (课程)的状态:
未被 DFS 访问:i == 0;
已被其他节点启动的 DFS 访问:i == -1;
已被当前节点启动的 DFS 访问:i == 1。对 numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。DFS 流程;
- 终止条件:
- 当 flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。
- 当 flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环 ,直接返回 False。
- 将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
- 递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回False;
- 当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 -1 并返回 True。
若整个图 DFS 结束并未发现环,返回 True。
//Lc207
class Solution {
//bfs
public boolean canFinish(int numCourses, int[][] prerequisites) {
int[] indegrees = new int[numCourses];
List<List<Integer>> adjacency = new ArrayList<>();
Queue<Integer> queue = new LinkedList<>();
for(int i = 0; i < numCourses; i++)
adjacency.add(new ArrayList<>());
// Get the indegree and adjacency of every course.
for(int[] cp : prerequisites) {
indegrees[cp[0]]++;
adjacency.get(cp[1]).add(cp[0]);
}
// Get all the courses with the indegree of 0.
for(int i = 0; i < numCourses; i++)
if(indegrees[i] == 0) queue.add(i);
// BFS TopSort.
while(!queue.isEmpty()) {
int pre = queue.poll();
numCourses--;
for(int cur : adjacency.get(pre))
if(--indegrees[cur] == 0) queue.add(cur);
}
return numCourses == 0;
}
//dfs(三种状态)
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> adjacency = new ArrayList<>();
for(int i = 0; i < numCourses; i++)
adjacency.add(new ArrayList<>());
int[] flags = new int[numCourses];
for(int[] cp : prerequisites)
adjacency.get(cp[1]).add(cp[0]);
for(int i = 0; i < numCourses; i++)
if(!dfs(adjacency, flags, i)) return false;
return true;
}
private boolean dfs(List<List<Integer>> adjacency, int[] flags, int i) {
if(flags[i] == 1) return false;
if(flags[i] == -1) return true;
flags[i] = 1;
for(Integer j : adjacency.get(i))
if(!dfs(adjacency, flags, j)) return false;
flags[i] = -1;
return true;
}
}
5. 动态规划
(1)、定义数组元素的含义
(2)、找出数组元素间的关系式(某一状态是怎么达到的)
(3)、找初始条件
1、子序列问题
//一般数组意义为以i为结尾
class Solution { //446 某个维度长度不确定+三个维度
public int numberOfArithmeticSlices(int[] nums) {
int ans = 0;
int n = nums.length;
Map<Long, Integer>[] f = new Map[n];
for (int i = 0; i < n; ++i) {
f[i] = new HashMap<Long, Integer>();
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < i; ++j) {
long d = 1L * nums[i] - nums[j];
int cnt = f[j].getOrDefault(d, 0);
ans += cnt;
f[i].put(d, f[i].getOrDefault(d, 0) + cnt + 1);
}
}
return ans;
}
}
2、编辑距离|字符串问题
3、路径问题
四个方向可扩的有环图的dp (576)
class Solution {
// 四个方向
int[][] dirs = new int[][] {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
// 取余
int MOD = 1000000007;
public int findPaths(int m, int n, int maxMove, int startRow, int startColumn) {
int[][][] dp = new int[m][n][maxMove + 1];
// 移动步数2的都是从移动步数1的转移来的
// 移动步数3的都是从移动步数2的转移来的
// 所以,要从移动步数从1开始递增
for (int k = 1; k <= maxMove; k++) {
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 处理四条边
if (i == 0) dp[i][j][k]++;
if (j == 0) dp[i][j][k]++;
if (i == m - 1) dp[i][j][k]++;
if (j == n - 1) dp[i][j][k]++;
// 中间的位置,向四个方向延伸
for (int[] dir : dirs) {
int nextI = i + dir[0];
int nextJ = j + dir[1];
if (nextI >= 0 && nextI < m && nextJ >= 0 && nextJ < n) {
dp[i][j][k] = (dp[i][j][k] + dp[nextI][nextJ][k - 1]) % MOD;
}
}
}
}
}
return dp[startRow][startColumn][maxMove];
}
}
4、背包问题
状态转移方程:f[i][j] = max(f[i-1][j],f[i-1][j-nums[i]+nums[i]])
- 01背包
每件物品只取一次
//416
//内层容量维度由大到小遍历,以保证每件物品取一次
public boolean canPartition(int[] nums) {
int n = nums.length;
//「等和子集」的和必然是总和的一半
int sum = 0;
for (int i : nums) sum += i;
int target = sum / 2;
// 对应了总和为奇数的情况,注定不能被分为两个「等和子集」
if (target * 2 != sum) return false;
// 将「物品维度」取消
int[] f = new int[target + 1];
for (int i = 0; i < n; i++) {
int t = nums[i];
// 将「容量维度」改成从大到小遍历
for (int j = target; j >= 0; j--) {
// 不选第 i 件物品
int no = f[j];
// 选第 i 件物品
int yes = j >= t ? f[j-t] + t : 0;
f[j] = Math.max(no, yes);
}
System.out.println(Arrays.toString(f));
}
// 如果最大价值等于 target,说明可以拆分成两个「等和子集」
return f[target] == target;
}
-
01背包并且考虑排列
即普通的排列问题(LC46) -
完全背包
每件物品可取多次
//322
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE-1;
int[] dp = new int[amount+1];
for(int i = 0;i<=amount;i++){
dp[i] = max;
}
dp[0] = 0;
//纯完全背包问题,只需看是否满足条件,考不考虑顺序皆可,因此两层循环谁先谁后皆可
for(int coin : coins){
for(int i = coin;i <= amount;i++){
dp[i] = Math.min(dp[i], dp[i-coin]+1);
}
}
return dp[amount] == max ? -1 : dp[amount];
}
- 完全背包(求组合数)
Lc518
for (int i = 0; i < coins.size(); i++) { // 遍历物品
for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量
dp[j] += dp[j - coins[i]];
}
}
//那么就是先把coins[0]加入计算,然后再把coins[1]加入计算,得到的方法数量只有{coins[0], coins[1]}这种情况。而不会出现{coins[1],coins[0]}的情况。
- 完全背包(完全背包并且考虑排列)
Lc377
for (int j = 0; j <= amount; j++) { // 遍历背包容量
for (int i = 0; i < coins.size(); i++) { // 遍历物品
if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]];
}
}
//背包容量的每一个值,都是经过coins[0]和coins[1]的计算,包含了{0, 1} 和 {1, 0}两种情况。此时dp[j]里算出来的就是排列数.
public int combinationSum4(int[] nums, int target) {
Arrays.sort(nums);
int[] dp=new int[target+1];
dp[0]=1;
for(int i=1;i<=target;i++){
for(int k:nums){
if(i-k>=0) dp[i]+=dp[i-k];
else break;
}
}
return dp[target];
}
5、数位Dp
Lc902
将问题抽象为求解一个[0,x]/[1,x] 范围方案数的方法 --> 对方案数统计根据 位数 来分情况讨论:数位相等的情况 + 数位不等情况 -> 统计数位相等的方案数时,需要按位处理,并根据限制条件做逻辑;统计数位不等的方案数时,通常要做一些预处理,然后配合乘法原理直接算得 。
class Solution {
int[] nums;
int dp(int x) {
List<Integer> list = new ArrayList<>();
while (x != 0) {
list.add(x % 10);
x /= 10;
}
int n = list.size(), m = nums.length, ans = 0;
// 位数和 x 相同
for (int i = n - 1, p = 1; i >= 0; i--, p++) {
int cur = list.get(i);
int l = 0, r = m - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (nums[mid] <= cur) l = mid;
else r = mid - 1;
}
if (nums[r] > cur) {
break;
} else if (nums[r] == cur) {
ans += r * (int) Math.pow(m, (n - p));
if (i == 0) ans++;
} else if (nums[r] < cur) {
ans += (r + 1) * (int) Math.pow(m, (n - p));
break;
}
}
// 位数比 x 少的
for (int i = 1, last = 1; i < n; i++) {
int cur = last * m;
ans += cur; last = cur;
}
return ans;
}
public int atMostNGivenDigitSet(String[] digits, int max) {
int n = digits.length;
nums = new int[n];
for (int i = 0; i < n; i++) nums[i] = Integer.parseInt(digits[i]);
return dp(max);
}
}
9. other
1、栈与计算器
//Lc227
2、合并链表
//Lc23
3、采样
- 拒绝采样
//Lc470
- 权值采样
//Lc528
void mergesort(int[] nums){
int len =nums.length;
for(int stride =1;stride<len;stride*=2){
int[] temp = new int[stride*2];
for(int i=0;i<len;i=i+stride*2){
int p=0,q=stride;
System.arraycopy(nums,i,temp,0,stride*2);
int pp=i;
while(p<stride&&q<stride*2){
if(temp[p]<=temp[q]) nums[pp++] = temp[p++];
else nums[pp++] = temp[q++]
}
while(p<stride) nums[pp++]=temp[p++];
while(q<stride*2) nums[pp++]=temp[q++];
}
}
}
while (l < r) {
int mid = l + r + 1 >> 1;
if (nums[mid] <= cur) l = mid;
else r = mid - 1;
}
if (nums[r] > cur) {
break;
} else if (nums[r] == cur) {
ans += r * (int) Math.pow(m, (n - p));
if (i == 0) ans++;
} else if (nums[r] < cur) {
ans += (r + 1) * (int) Math.pow(m, (n - p));
break;
}
}
// 位数比 x 少的
for (int i = 1, last = 1; i < n; i++) {
int cur = last * m;
ans += cur; last = cur;
}
return ans;
}
public int atMostNGivenDigitSet(String[] digits, int max) {
int n = digits.length;
nums = new int[n];
for (int i = 0; i < n; i++) nums[i] = Integer.parseInt(digits[i]);
return dp(max);
}
}
## 9. other
##### 1、栈与计算器
//Lc227
##### 2、合并链表
//Lc23
##### 3、采样
- 拒绝采样
//*Lc470*
- 权值采样
//*Lc528*
```java
void mergesort(int[] nums){
int len =nums.length;
for(int stride =1;stride<len;stride*=2){
int[] temp = new int[stride*2];
for(int i=0;i<len;i=i+stride*2){
int p=0,q=stride;
System.arraycopy(nums,i,temp,0,stride*2);
int pp=i;
while(p<stride&&q<stride*2){
if(temp[p]<=temp[q]) nums[pp++] = temp[p++];
else nums[pp++] = temp[q++]
}
while(p<stride) nums[pp++]=temp[p++];
while(q<stride*2) nums[pp++]=temp[q++];
}
}
}