【算法+LeetCode题解+剑指Offer题解】算法思想、排序搜索、数据结构、数学、特殊解法、设计(全总结)

文章目录

  • 前言
  • 参考
  • 一. 排序
    • 1. 快速排序
    • 2 归并排序
    • 3 堆排序
    • 4 其他排序
    • 5. 第K大/小问题(快速选择算法)
    • 6. 有序数据合并问题
  • 二、二分查找
    • 1. 基本二分查找
    • 2. 二分查找左边界
    • 3. 二分查找右边界
    • 4. 相关题目
  • 三、搜索
    • 1. DFS
    • 2. 回溯
    • 3. BFS
    • 4. 排列组合问题
    • 5. 矩阵搜索问题
    • 6. 其他DFS问题
  • 四、树与递归

前言

一个多月的时间,看完了LeetCode的题目,刷完了主要的题目。越看到后面,越觉得很多题目都是非常相似的,很多题目都是通过基本的思想扩展来的,因此需要对题型进行总结。
算法题目一般分为以下几类,常见的用红色背景标出:

  1. 算法思想:贪心、动态规划、分治、递归
  2. 排序和搜索:排序二分查找DFSBFS回溯
  3. 数据结构:字符串数组/矩阵、队列、哈希表链表二叉树、图
  4. 数学:位运算、概率计算、数字处理、经典算法
  5. 特殊解法:滑动窗口双指针
  6. 设计:LRU、LFU、满足O(1)的数据结构组合

参考

  1. https://github.com/labuladong/fucking-algorithm
    这个项目详细介绍了LeetCode题型和解题模板
  2. https://github.com/liguigui/CyC2018-CS-Notes
    这个项目的算法部分总结了题型和代表性题目
  3. 《剑指Offer:名企面试官精讲典型编程题(第2版)》
    这本书不仅介绍了各种题型、数据结构,而且从题目理解、沟通交流、思路产生、代码编写、算法优化等方面全方位地告诉读者如何参与一场算法面试。

更多参考文献、书籍、网址、博客写在文中对应部分

一. 排序

  • 常见的8种排序方法可以参考这道题目,里面有各种排序方法的标准实现
    LeetCode 912. 排序数组
  • 参考题解 复习基础排序算法(Java),十二种排序算法包你满意(带GIF图解)
  • 常考的排序有快速排序,归并排序,堆排序三种,是必须要掌握的,包括算法的各种细节部分。

1. 快速排序

// 快速排序,随机选择1个数作为pivot,放在最左边,安置到数组合适的位置,使其大于左边的全部元素,小于右边的全部元素
// 时间O(nlogn),空间O(logn)
private void quickSort(int[] nums, int left, int right){
     
    swap(nums,left,left+rand.nextInt(right-left+1));//随机挑选一个数作为pivot
    if(left>=right) return;
    int p = left;//p指针指向小于等于pivot的第一个元素
    for(int i = left+1 ; i <= right ; i++){
     
        //如果有小于等于pivot的元素,与p+1交换(因为p+1指向的是大于pivot的第一个元素)
        if(nums[i] <= nums[left]){
     
            swap(nums,++p,i);
        }
    }
    //最后将left与p交换,p就是pivot的索引
    swap(nums,left,p);
    //继续排序左边和右边的区间
    quickSort(nums,left,p-1);
    quickSort(nums,p+1,right);
}

2 归并排序

// 归并排序,temp为全局临时数组,这样可以避免反复开辟空间
private void mergeSort(int[] nums, int left, int right, int[] temp){
     
    if(left>=right) return;
    int mid = left + (right-left)/2;
    mergeSort(nums,left,mid,temp);
    mergeSort(nums,mid+1,right,temp);
    if(nums[mid]<=nums[mid+1]) return;//如果两个区间已经有序,就不需要合并了。
    int i = left, j = mid+1, k = 0;
    while(i<=mid && j<=right) temp[k++] = nums[i]<nums[j]?nums[i++]:nums[j++];
    while(i<=mid) temp[k++] = nums[i++];
    while(j<=right) temp[k++] = nums[j++];
    System.arraycopy(temp,0,nums,left,right-left+1);  
}

3 堆排序

private void heapSort(int[] nums){
     
    //首先建立大顶堆,对于每一个非叶子节点,从下至上构建大顶堆
    for(int i = nums.length/2-1 ; i>=0 ;i--){
     
        adjustHeap(nums,i,nums.length-1);
    }
    //把堆顶的最大值放置到尾部,然后继续进行调整堆操作
    int end = nums.length-1;
    while(end>0){
     
        swap(nums,0,end);//交换头尾元素
        end--;
        adjustHeap(nums,0,end);//继续调整堆
    }

}

//调整节点i及其后续节点使其满足大顶堆定义,调整范围为[0,end]
private void adjustHeap(int[] nums, int i, int end){
     
    int k = i;
    //如果k存在子节点
    while(k*2+1 <= end){
     
        int j = 2*k + 1;//j指向左子节点
        //如果右子节点存在,并且右子节点大于左子节点,那么把j指向右子节点(即j指向子节点中的较大值)
        if(j+1<=end && nums[j+1]>nums[j]){
     
            j++;
        }  
        //如果根节点小于子节点,交换,让大的值上浮
        if(nums[k]<nums[j]){
     
            swap(nums,k,j);
            //交换后,由于改变了子节点,因此从子节点继续向下判断是否满足
            k = j;
        }else{
     
            break;
        }
    }
}

4 其他排序

名称 时间复杂度 解释
冒泡排序 O(n^2) 遍历n-1次数组,每次遍历就把最大的放在末尾,末尾元素不会进入下一次遍历。可以添加isSorted变量,如果在一次遍历中没有交换元素,就代表有序了,不用继续向下遍历。
选择排序 O(n^2) 每次选择最大的元素,然后跟末尾元素交换即可,末尾元素不会进入下一次遍历。
插入排序 O(n^2) 从i=1开始,依次将元素插入到前面的有序数组之中。
希尔排序 O(n^2) 对插入排序的优化,对数据按间隔分组,组内不断进行插入排序,然后逐渐缩小间隔到1
桶排序 O(n) 设置若干个桶,使用一个映射函数将所有元素放入固定大小的桶中,然后在桶内部可以使用任意排序方法对所有元素进行排序,最后依次从全部桶中拿出所有元素即可。两种主要的桶排序方法是计数排序和基数排序。
计数排序 O(n) 使用辅助数组记录每个元素出现的次数,然后复原即可,要求待排序数组的值在一个适当的范围
基数排序 O(n) 根据元素的数位来分配桶,例如0到9十个桶代表元素的十位数字,那么(0至9)分配到桶0,(10至19)分配到桶1,…

5. 第K大/小问题(快速选择算法)

第K大是快速排序算法的应用,称为快速选择算法,不需要完全排序,只需要在某一次安置pivot时,pivot的索引刚好等于k即可,如果不等于k,只需要对其中一个区间继续搜索。

private int findKthLargest(int[] nums ,int left, int right, int k){
     
    swap(nums,left,left+rand.nextInt(right-left+1));
    if(left>=right) return nums[left];
    int p = left;
    for(int i = left+1;i<=right;i++){
     
        if(nums[i] < nums[left]){
     
            swap(nums,i,++p);
        }
    }
    swap(nums,p,left);
    if(p == nums.length-k){
     
        return nums[p];
    }else if(p > nums.length-k){
     
        return findKthLargest(nums,left,p-1,k);
    }else{
     
        return findKthLargest(nums,p+1,right,k);
    }
}
题目 方法
LeetCode 215. 数组中的第K个最大元素 标准的求第K大
LeetCode 324. 摆动排序 II 使用快速选择算法求出中位数,求中位数等价于求第k大,k为length/2,数组的后半部分大于前半部分,然后依次取一个,构造摆动排序
LeetCode 347. 前 K 个高频元素 前K大问题,数组变成了矩阵或Entry数组
LeetCode 462. 最少移动次数使数组元素相等 II 这道题目的核心就是求中位数,求中位数等价于求第k大,k为length/2,同样可以使用快速选择算法
LeetCode 973. 最接近原点的 K 个点 同样是求前K小问题 ,只是数组变成了矩阵

6. 有序数据合并问题

题目 方法
LeetCode 21. 合并两个有序链表 按照归并排序的merge算法,如果有剩余数据,可以直接接在后面,不用再逐个复制
LeetCode 88. 合并两个有序数组 从后向前合并,三指针
LeetCode 148. 排序链表 归并排序链表
LeetCode 315. 计算右侧小于当前元素的个数 在merge的时候计算逆序对
剑指 Offer 51. 数组中的逆序对 跟上题一样

二、二分查找

1. 基本二分查找

public int search(int[] nums, int target) {
     
    int left= 0;
    int right = nums.length - 1; // 注意
    while (left <= right) {
      //注意
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
     
            return mid;
        } else if (nums[mid] > target) {
     
            right = mid - 1;
        } else {
     
            left = mid + 1;
        }
    }
    return -1;
}

2. 二分查找左边界

public intsearch(int[] nums, int target) {
     
    int left = 0 ;
    int right = nums.length - 1;
    while(left <= right){
     
        int mid = left + (right-left)/2 ;
        if(nums[mid] >= target){
     
            right = mid - 1;
        }else{
     
            left = mid + 1;
        }
    }
    if(left == nums.length || nums[left]!=target){
     
        return -1;
    }
    return left;
}

3. 二分查找右边界

public int search(int[] nums, int target) {
     
    int left = 0 ;
    int right = nums.length - 1;
    while(left <= right){
     
        int mid = left + (right-left)/2 ;
        if(nums[mid] <= target){
     
            left = mid + 1;
        }else {
     
            right = mid - 1;
        }
    }
    if(right == -1 || nums[right]!=target){
     
        return -1;
    }
    return right;
}

4. 相关题目

题目 方法
LeetCode 34. 在排序数组中查找元素的第一个和最后一个位置 查找左右边界
LeetCode 35. 搜索插入位置 直接二分查找
LeetCode 69. x 的平方根 在1到x二分查找
LeetCode 74. 搜索二维矩阵 有序矩阵的二分查找,注意索引转换
LeetCode 33. 搜索旋转排序数组 数组部分有序也可以使用二分查找,只要能确定下个查找区间
LeetCode 153. 寻找旋转排序数组中的最小值 和上题一样
LeetCode 162. 寻找峰值 如果是峰值就返回,如果是上升序列,就在右边找,否则在左边找
LeetCode 278. 第一个错误的版本 二分找左边界
LeetCode 367. 有效的完全平方数 1到x二分查找
LeetCode 540. 有序数组中的单一元素 把与之相邻的重复元素拿掉后,选择区间长度为奇数的继续查找
LeetCode 658. 找到 K 个最接近的元素 二分查找定位最接近target的元素
LeetCode 744. 寻找比目标字母大的最小字母 找右边界

三、搜索

搜索主要包括广度优先搜索深度优先搜索,回溯属于深度优先搜索。搜索很多时候相当于是暴力的解法,遍历所有可能性,因此解题模式比较套路化。

1. DFS

可以定义全局变量如count,list,hashmap等,表示每一步做出的修改,比如添加一个结果,或者计数加1等。。。
//可以从一个或多个起点调用dfs
dfs(...);
//定义dfs函数,返回值表示需要向上一步返回哪些内容,参数表示上一步向当前这个步传递了哪些信息
返回值 dfs(参数){
     
	if(达到边界条件) return;
	if(搜索到一个符合条件的结果){
     
		添加结果到集合、更新共享变量、向上一步返回一些信息等。。。
		return;
	}
	没有搜索到,就继续向下搜索,先获取或计算可以做出的选择(能够传递给下一层的参数)
	for (选择 in 选择列表){
     
		dfs(参数、选择...)
	}
}

2. 回溯

和DFS基本类似,主要多了一个恢复状态的过程

result = []
def backtrack(路径,选择列表):
	if 满足结束条件:
		result.add(路径)
		return
	for 选择 in 选择列表:
		做选择
		backtrack(路径,选择列表)
		撤销选择

3. BFS

BFS需要结合队列使用

void bfs(Node start, Node target) {
     
    初始化队列q
    队列中添加初始1个或多个值 q.offer()...
    while (队列不为空){
     
    	//每次while循环都是新的一层
        获取队列大小size
        for(int i = 0 ;i<size;i++){
     
            Node cur = q.poll();
            弹出每个节点,对每个节点进行处理
            然后将每个节点相邻的节点继续加入队列
        }
    }
}

4. 排列组合问题

题目 方法
LeetCode 46. 全排列(不含重复元素) 回溯,做选择后把元素交换到前面,然后传递给下一步开始做选择的索引
LeetCode 47. 全排列 II(包含重复元素) 回溯,每一步的选择需要去重,即提前剪枝
LeetCode 39. 组合总和(无重复的正整数集合,可重复选) 下一步的候选集合从i开始
LeetCode 40. 组合总和 II(包含重复的正整数集合,不可重复选) 先排序,在进行选择的时候去重
LeetCode 216. 组合总和 III 注意结束条件判断
LeetCode 78. 子集(不含重复元素) 标准回溯算法
LeetCode 90. 子集 II(包含重复元素) 和LeetCode40题一样
LeetCode 17. 电话号码的字母组合 标准回溯问题

5. 矩阵搜索问题

题目 方法
LeetCode 37. 解数独 在每一个空白格子尝试每一种可能
LeetCode 51. N 皇后 每一行有n个列可供选择,尝试每一种可能
LeetCode 79. 单词搜索 在每个位置进行DFS,需要用visit矩阵储存是否访问
LeetCode 130. 被围绕的区域 搜索所有与边界O相连的O
LeetCode 200. 岛屿数量 在值为1的位置开始dfs,把与之相邻的变为其他数字
LeetCode 329. 矩阵中的最长递增路径 使用辅助数组储存从(i,j)出发的最长路径

6. 其他DFS问题

题目 备注
LeetCode 22. 括号生成 记录左右括号的数量
LeetCode 93. 复原IP地址 注意筛选每一步的选择,还有提前剪枝的判断
LeetCode 282. 给表达式添加运算符
LeetCode 306. 累加数
LeetCode 401. 二进制手表 构造选择集,注意去重

四、树与递归

你可能感兴趣的:(算法)