算法模板整理

文章目录

  • 二叉树
    • 总模板
    • 二分搜索树模板
  • 单调栈
    • 基础模板
    • 循环数组模板
  • 单调队列
  • 二分查找
    • 二分查找模板
      • 基本二分查找
      • 左侧边界二分查找
      • 右侧边界二分查找
    • 双指针
      • 快慢指针
      • 左右指针
    • 滑动窗
      • 滑动窗模板
  • 回溯算法
    • 回溯模板
      • 全排列-直接套用模板
    • 回溯+备忘录模板
      • 全排列-备忘录回溯
    • 回溯+剪枝模板
      • 全排列||-剪枝回溯
  • 动态规划
    • 子序列问题模板
      • 矩形路径
    • 背包问题
      • 01背包
      • 完全背包
      • 二维费用
    • 贪心算法模板
  • 回溯与动态规划
  • 排序
    • 冒泡排序
      • 经典冒泡排序
    • 选择排序
    • 插入排序
    • 希尔排序
    • 归并排序
      • 自顶向下
      • 自底向上
    • 快速排序
      • 经典实现
      • 双路快排
      • 三路快排
    • 堆排序这个好像不是重点
    • 交换方法
  • 单例模式
    • 1.饿汉式单例模式
    • 2.懒汉式单例模式
    • 3.懒汉式(双重检查加锁版本)
    • 4.内部类单例模式
    • 5.枚举
  • LRU
    • 算法描述
    • 算法实现

二叉树

总模板

总路线:明确一个节点要做的事情,然后剩下的时抛给框架

void traverse(TreeNode node){
     
    //node节点需要做的事情,在这做
    toDo(...);
    //其他的节点不用node操心,抛给框架
    traverse(node.left);
    traverse(node.right);
}

代码实现:

  1. 二叉树所有的节点值加1

    void plusOne(TreeNode node){
           
        //终止条件
        if(node == null) return;
        
        //node具体的操作
        root.val += 1;
        
        //其余节点
        plusOne(node.left);
        plusOne(node.right);
    }
    
  2. 判断二叉树是否相同

    boolean isSameTree(TreeNode node1, TreeNode node2){
           
    	
        //两个node节点的具体操作
        if(node1 == null && node2 == null) return true;//都为空,则显然相同
        if(node1 == null || node2 == null) retrun false;//一个空一个非空,显然不同
        if(node1.val != node2.val) return false;//两个都非空,但val不同也不同
        
        //其余节点
        return isSameTree(node1.left,node2.left) && isSameTree(node1.right,node2.right);
    }
    

二分搜索树模板

定义:一个二叉树中,任意节点的值要大于等于左子树所有节点的值,且要小于等于右边子树的所有节点的值。

BST的遍历:

void BST(TreeNode node, int target){
     
    if(node.val == target)
        //找到目标做点什么
    if(node.val < target)
        BST(node.right,target);
    if(node.val > target)
        BST(node.left,target);
}

单调栈

基础模板

以Next Greater Number为例作为模板(参考labuladong的算法小抄)

给出:[2,1,2,4,3] 返回:[4,2,4,-1,-1]

ArrayList<Integer> nextGreaterElement(int[] nums){
     
    List<Integer> ans = new ArrayList<>();
    Stack<Integet> stack = new Stack<>();
    
    for(int i=nums.length-1; i>=0; i--){
     //倒着入栈,正着出栈
        while(!stack.isEmpty() && stack.top() <= nums[i]){
     //判定个子高矮
            stack.pop(); //矮个被弹出,因为会被高的挡住
        }
        //拿到这个元素身后的第一高个
        ans.get(i) = stack.isEmpty() ? -1 : stack.peek(); 
        stack.push(nums[i]);//进队
    }
    return ans;
} 

:这个算法的时间复杂度为O(N),因为while循环会弹出元素,每个元素都会入栈一次最多也只会被出栈一次,没有任何多余操作故为O(N)的时间复杂度

循环数组模板

当上面的数组是以环形的方式存放的则可以使用双倍长度数组来进行模拟

线性数组模拟环形特效

int n = nums.length,index = 0;
while(true){
     
    print(nums[index % n]);
    index++;
}

环形Next Greater Number

ArrayList<Integer> nextGreaterElement(int[] nums){
     
    int n = nums.length;
    List<Integer> ans = new ArrayList<>();
    Stack<Integet> stack = new Stack<>();
    
    for(int i=2*n-1; i>=0; i--){
     //假装数组长度翻倍
        while(!stack.isEmpty() && stack.top() <= nums[i%n]){
     //通过取余来实现环形
            stack.pop(); //矮个被弹出,因为会被高的挡住
        }
        //拿到这个元素身后的第一高个
        ans.get(i%n) = stack.isEmpty() ? -1 : stack.peek(); 
        stack.push(nums[i]);//进队
    }
    return ans;
} 

单调队列

本质上仍是一个队列,只不过队列中的元素递增或递减,通过这样的一个数据结构可以解决滑动窗的一系列问题

以LeetCode-239题目为例,重点在于线性时间复杂度的实现

前提结论:

​ 在一堆数字中,已知最值,如果给这堆数添加一个数,那么比较一下就可以很快计算出最值;但如果减少一个数则需要遍历所有数,重新找最值

解题模板:

public int[] maxSlidingWindow(int[] nums, int k) {
     
	Window window;
    int[] res = new int[nums.length-k+1];
    
    for(int i=0; i<nums.length;i++){
     
        if(i < k-1){
     //先把窗口的前k-1填满
            window.push(nums[i]);
        }else{
     //窗口开始滑动
            window.push(nums[i]);
            res[i] = window.max(); //窗口里的最大值
            window.pop(nums[i-k+1]); //移除窗口中最左侧元素
        }    
    }
    return res;
}

window结构可以使用单调队列来找寻最值

//Java中LinkedList用来实现双向队列的
LinkedList<Integer> list = new LinkedList();

for(int i=0; i<nums.length; i++){
     
    //保证从左至右是从大到小的,如果前面的数小于当前的则弹出
    while(list.isEmpty() && nums[list.peekLast()] <= nums[i])
        list.pollLast();
    //添加对应元素的下标到队列中
    list.addLast(i);
    //当窗口长度超过k时则需要删除过期的元素
    if(list.peek()<= i-k)
        list.poll();
}

单调队列与优先级队列:

​ 单调队列在添加元素的时候靠删除元素保持队列的单调性,相当于抽取出某个函数中单调递增(递减)部分;优先级队列(二叉堆)相当于自动排序

二分查找

分析二分查找:不要出现else,而是把所有情况用else if 写清楚,这样可以清楚地展现所有细节

二分查找模板

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

基本二分查找

查询一个数,判断是否存在

public int binarySearch(int[] nums, int target){
     
    int left = 0;
    int right = nums.length-1; //right的起始位置会影响while循环的结束条件
    
    while(left <= right){
     
        int mid = left+(right-left)/2;
        if(nums[mid] == target){
     
            return mid;
        }else if(nums[mid] < target){
     
            left = mid+1;
        }else if(nums[mid] > target){
     
            right = mid-1;
        }
    }
    //不存在时返回-1
    return -1;
} 

这个最基本的二分搜索,存在局限性:无法处理重复元素时的情况,下面两种方法则可以处理这类情况

左侧边界二分查找

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

右侧边界二分查找

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

双指针

双指针还可以细化分为两类:快慢指针和左右指针

快慢指针

  1. 链表是否有环

    public boolean hasCysle(ListNode head){
           
    	ListNode fast,slow;
        fast = slow = head;
        
        while(fast != null && fast.next != null){
           
            fast = fast.next.next;
            slow = slow.next;
            
            if(fast == slow) return true;
        }
        return false;
    }
    
  2. 判断是否有环并返回入环节点

    public ListNode detectCycle(ListNode head) {
           
        ListNode slow = head,fast = head;
        boolean cycle = false;
        
        while (fast != null && fast.next != null){
           
        	if (cycle && slow == fast) return slow;
       		slow = slow.next;
        	if (!cycle) 
                fast = fast.next.next;
        	else  fast = fast.next;
        	if (fast == slow ){
           
        		if (cycle) return slow;
        		cycle = true;
        		slow = head;
        	}
        }
        return null;
    }
    
  3. 链表的中间节点

    while (fast != null && fast.next != null){
           
        fast = fast.next.next;
        slow = slow.next;
    }
    //slow即在中间节点位置
    return slow;
    
  4. 寻找链表倒数第k个元素

    ListNode slow,fast;
    slow = fast = head;
    
    while(k-- > 0){
           
        fast = fast.next;
    }
    while(fast != null){
           
        slow = slow.next;
        fast = fast.next;
    }
    
    return slow;
    

左右指针

左右指针实际上是两个索引值,一般初始化为

left = 0,right = nums.lenght-1;

典型的如二分查找就是利用了左右指针,只要数组是有序的,就应该考虑到数组,某些情况下即使是无序的数组也可考虑先排序

  1. LeetCode 1 两数之和

    public int[] twoSum(int[] nums, int target){
           
        int left = 0,right = nums.length-1;
        Arrays.sort(nums);
        
        while(left < right){
           
            int sum = nums[left] + nums[right];
            if(sum == target){
           
                return new int[]{
           left,right};
            }else if(sum < target){
           
                left++;
            }else if(sum > target){
           
                right--;
            }
        }
        //当没有答案时
        return new int[]{
           -1,-1};
    }
    

    注:上面的方法不一定是最优的,重点是左右指针的思路

  2. 反转数组

    public void reverse(int[] nums){
           
        int left = 0,right = nums.length-1;
        
        while(left < right){
           
            //自己写的交换方法
            swap(nums,left,right);
            left++;right--;
        }
    }
    

滑动窗

滑动窗模板

以LeetCode中寻找子串为例

int left = 0, right = 0;
while (right < s.size()){
      #字符串右边界
    window.add(s[right]);
    right++;
    while(题中条件){
     
        window.remove(s[left]);
        left++;
    }
}

window的数据类型要根据题目的具体情况而定的,比较麻烦的就是while中的条件,不同题目下会有具体的变化,有可能会条件会很复杂

回溯算法

回溯算法的本质就是多叉树的遍历问题,关键就是在前序和后序遍历的位置需要做一些操作

def backtrack(...):
	for 选择 in 选择列表:
		做选择
		backtrack(...)
		撤销选择

回溯模板

    result = []

    def 主方法(传入的参数)
    backtrack(路径,选择列表)

    def backtrack(路径,选择列表):
        if 满足结束条件:
        result.add(路径)
        return

    for 选择 in 选择列表:
    # 做选择
    将该选择从选择列表中移除
    路径.add(选择)
    backtrack(路径,选择列表)
    # 撤销选择
    路径.remove(选择)
    将该选择再加入选择列表

注意:

  1. 对于回溯并不是一定要有for循环从列表中选择的,有些选择可以直接举例有些选择
  2. 对于回溯后的返回常在方法中添加backtrack(旧路径+新路径)这样返回方法后可以回到未添加前的状态
  3. 对于自增若需要回到未添加前的状态,在方法中使用+1
  4. 当腰排除重复情况时,需要控制好起始位置

全排列-直接套用模板

class Solution {
     
    //返回结果
    List<List<Integer>> res = new LinkedList<>();
    //主方法
    public List<List<Integer>> permute(int[] nums) {
     
        LinkedList<Integer> track = new LinkedList<>();
        backtrack(nums,track);
        return res;
    }

    private void backtrack(int[] nums,LinkedList<Integer> track){
     
        //结束条件
        if(track.size() == nums.length){
     
            res.add(new LinkedList(track));
            return;
        }

        for(int i=0; i<nums.length;i++){
     
            //排除不合法的选择,这里稍微变通了下,并没有显示记录选择列表
            if(track.contains(nums[i]))
                continue;
            track.add(nums[i]);
            backtrack(nums,track);
            //取消选择
            track.removeLast();
        }
    }
}

回溯+备忘录模板

 	result = []
    memo = dict() # 定义一个备忘录
    
    def 主方法(传入的参数)
        backtrack(路径,选择列表)
       
    def backtrack(路径,选择列表):
        if 满足结束条件:
            result.add(路径)
            return

        for 选择 in 选择列表:
            # 做选择
            将该选择加入备忘录
            路径.add(选择)
            backtrack(路径,选择列表)
            # 撤销选择
            路径.remove(选择)
            将该选择从备忘录中移除

全排列-备忘录回溯

class Solution {
    //返回结果
    List> res = new LinkedList<>();
    //主方法
    public List> permute(int[] nums) {
        LinkedList track = new LinkedList<>();
        //放入一个备忘录
        int[] memo = new int[nums.length];
        backtrack(nums,track,memo);
        return res;
    }

    private void backtrack(int[] nums,LinkedList track,int[] memo){
        //结束条件
        if(track.size() == nums.length){
            res.add(new LinkedList(track));
            return;
        }

        for(int i=0; i

回溯+剪枝模板

剪枝所依赖的还是备忘录,只是判断条件复杂了一些,这个题目参考LeetCode-47 全排列||

 	result = []
    memo = dict() # 定义一个备忘录
    
    def 主方法(传入的参数)
    	排序
        backtrack(路径,选择列表)
       
    def backtrack(路径,选择列表):
        if 满足结束条件:
            result.add(路径)
            return

        for 选择 in 选择列表:
            # 做选择
            根据备忘录进行剪枝
            将该选择加入备忘录
            路径.add(选择)
            backtrack(路径,选择列表)
            # 撤销选择
            路径.remove(选择)
            将该选择从备忘录中移除

全排列||-剪枝回溯

剪枝:减去出现重复可能性,在此处即减去上次出现过相同数字的情况,由于在不同的题目中灵活性很高,这里我还没有有较好的总结

class Solution {
     
    //返回结果
    List<List<Integer>> res = new LinkedList<>();

    public List<List<Integer>> permuteUnique(int[] nums) {
     
        LinkedList<Integer> track = new LinkedList<>();
        //放入一个备忘录
        int[] memo = new int[nums.length];
        Arrays.sort(nums);
        backtrack(nums,track,memo);
        return res;
    }

    private void backtrack(int[] nums,LinkedList<Integer> track,int[] memo){
     
        //结束条件
        if(track.size() == nums.length){
     
            res.add(new LinkedList(track));
            return;
        }

        for(int i=0; i<nums.length;i++){
     
            //使用数组查找比链表查询要快很多
            if(memo[i] == 1)
                continue;
            //根据备忘录进行剪枝
            if(i>0 && nums[i] == nums[i-1] && memo[i-1]==0)
                continue;
            // 将选择添加进备忘录
            memo[i] = 1;
            track.add(nums[i]);
            backtrack(nums,track,memo);
            //取消选择
            track.removeLast();
            //将选择从备忘录中移除
            memo[i] = 0;
        }
    }
}

动态规划

子序列问题模板

一维dp数组

int n = array.length;
int[] dp = new int[n];
//dp[i]:在子数组array[0...i]中,要求的子序列的目标(如:最长递增子序列中表示长度)
for (int i = 1; i < n; i++) {
     
    for (int j = 0; j < i; j++) {
     
        dp[i] = 最值(dp[i],dp[j]+限制条件)
    }
}

二维dp数组

int n = array.length;
int[][] dp = new int[n][n];
//dp[i][j]:在子数组arr1[0...i]中和子数组arr2[0...j]中,要求的子序列的目标(如:最长公共子序列中表示长度)
for (int i = 0; i < n; i++) {
     
    for (int j = 0; j < n; j++) {
     
        if (arr[i] == arr[j])
            dp[i][j] = dp[i][j]+限制条件
        else
            dp[i][j] = 最值(...}
}

矩形路径

public static int minPath(int[][] m) {
     
    if (m == null || m.length == 0 || m[0] == null || m[0].length == 0) {
     
        return 0;
    }
    int row = m.length;
    int col = m[0].length;
    int[][] dp = new int[row][col];
    dp[0][0] = m[0][0];
    //先填充行
    for (int i = 1; i < row; i++) {
     
        dp[i][0] = dp[i - 1][0] + m[i][0];
    }
    //再填充列
    for (int j = 1; j < col; j++) {
     
        dp[0][j] = dp[0][j - 1] + m[0][j];
    }
    //一般位置的填充
    for (int i = 1; i < row; i++) {
     
        for (int j = 1; j < col; j++) {
     
            dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + m[i][j];
        }
    }
    return dp[row - 1][col - 1];
}

背包问题

01背包

特点:每种物品仅有一件,可以选择放或不放

状态转移方程:

算法模板整理_第1张图片

代码实现:

public int solve(int[] w,int[] v,int C){
     
        int n = w.length;
        if (n == 0){
     
            return 0;
        }
        // 对记录数组进行初始化,因为容量时从0到C的,因此可以第二维的长度应该是C+1
        int[][] dp = new int[n][C+1];
        // 先把初始的填充了
        for (int j = 0; j <= C ; j++) {
     
            if(w[0] <= j)
            	dp[0][j] = v[0];
        }
        for (int i = 1; i < n; i++) {
     
            for (int j = 0; j <= C; j++) {
     
                // 策略1 不放新物品
                dp[i][j] = dp[i-1][j];
                // 策略2 放新物品但要先判断
                if (j >= w[i]){
     
                    dp[i][j] = Math.max(dp[i][j],v[i]+dp[i-1][j-w[i]]);
                }
            }
        }
        return dp[n-1][C];
    }

完全背包

**特点:**每种物品有无限件,策略为取0件、1件、2件等等

状态转移方程:
算法模板整理_第2张图片

二维费用

**特点:**每件物品有两种不同的费用,选择这件物品时必须同时付出这两种费用

状态转移方程:

在这里插入图片描述

f[i][v][u]表示前i件商品付出两种v和u费用时可以获得的最大值

代码实现:

贪心算法模板

区间调度

如:[start,end]的闭区间,找出这些区间中最多有几个互不相交的区间

public int intervalSchedule(int[][] intervals){
     
    if(intervals.length == 0) return 0;
    //重写比较方法,按照end结尾排序
    Arrays.sort(intervals,new Comparator<int[]>(){
     
        public int compare(int[] a,int[] b){
     
            return a[1] - b[1];
        }
    });
    
    //至少有一个区间不相交
    int count = 1;
    //排序后,第一个区间就是x
    int x_end = intervals[0][1];
    for(int[] interval : intervals){
     
        int start = interval[0];
        //下一个区间的起始要大于本次区间的结尾
        if(start >= x_end){
     
            //找到下一个选择的区间
            count++;
            x_end = interval[1];
        }
    }
    return count;
}

回溯与动态规划

动态规划需要明确:状态、选择、basecase

回溯需要明确:走过的路径、当前的选择列表、结束条件

从上面可以看出二者是很类似的,只不过在对于处理重叠子问题上有不同的策略:动态规划自顶向下

,回溯则是进行剪枝

可以适当总结:

  • 求最终结果:动态规划
  • 求过程:回溯

排序

冒泡排序

* 思路:依次比较相邻的两个数,将小数放在前面,大数放在后面。即在第一趟:首先比较第1个和第2个数,将小数放前,大数放后。
* 然后比较第2个数和第3个数,将小数放前,大数放后,如此继续,直至比较最后两个数,将小数放前,大数放后。
* 重复第一趟步骤,直至全部排序完成。
*
* 第一趟比较完成后,最后一个数一定是数组中最大的一个数,所以第二趟比较的时候最后一个数不参与比较;
*
* 第二趟比较完成后,倒数第二个数也一定是数组中第二大的数,所以第三趟比较的时候最后两个数不参与比较;
*
* 依次类推,每一趟比较次数-1;
*

经典冒泡排序

public static void sort(Comparable[] arr){
     
    int n = arr.length;
    if (arr == null || n < 2){
     
        return;
    }
    //最外层的循环控制结束位置
    for (int i = n-1; i >0 ; i--) {
     
        //内层循环负责两两交换
        for (int j = 0; j < i; j++) {
     
            if (arr[j].compareTo(arr[j+1]) > 0){
     
                swap(arr,j,j+1);
            }
        }
    }
}

改进

​ 加入一个是否交换的标志,当不发生交换时即有序了此时可以终止排序

public static void sort(Comparable[] arr){
     
        int n = arr.length;
        if (arr == null || n < 2){
     
            return;
        }

        for (int i = 0; i < n; i++) {
     
            // 是否交换的标志
            boolean swaped = false;
            for (int j = 0; j < n-1-i; j++) {
     
                if (arr[j].compareTo(arr[j+1]) > 0){
     
                    swap(arr,j,j+1);
                    swaped = true;
                }
            }
            // 当没有交换发生时便结束循环
            if (swaped == false){
     
                break;
            }
        }
    }
 }

选择排序

    1、从第一个元素开始,分别与后面的元素向比较,找到最小的元素与第一个元素交换位置;

  2、从第二个元素开始,分别与后面的元素相比较,找到剩余元素中最小的元素,与第二个元素交换;

  3、重复上述步骤,直到所有的元素都排成由小到大为止。
public static void sort(Comparable[] arr){
     
    if (arr == null || arr.length < 2) {
     
        return;
    }
    int n = arr.length;
    for (int i = 0; i < n - 1; i++) {
     
        //寻找[i,n)区间里的最小值的索引
        int minIndex = i;
        for (int j = i+1; j < n; j++) {
     
            //使用compareTo方法比较
            if (arr[j].compareTo(arr[minIndex]) < 0) {
     
                minIndex = j;
            }
        }
        swap(arr, i, minIndex);
    }
}

插入排序

/*首先来解释一下插入排序法的原理,它的原理是每插入一个数都要将它和之前的已经完成排序的序列进行重新排序,也就是要找到新插入的数对应原序列中的位置。那么也就是说,每次插入一个数都要对原来排序好的那部分序列进行重新的排序,时间复杂度同样为O(n²)。 这种算法是稳定的排序方法。*/
public static void sort(Comparable[] arr){
     
    if (arr == null || arr.length < 2){
     
    	return;
    }
    
    int n = arr.length;
    for (int i = 1; i < n; i++) {
     
    //arr[j].compareTo(arr[j-1]) < 0这个判断放在for上要比在for循环内部判断的效率要高很多,这样实现了提前跳出循环
    for (int j = i; j > 0 && (arr[j].compareTo(arr[j-1]) < 0); j--) {
     
    	swap(arr,j,j-1);
    }
}

改进:

​ 因为一次交换带来三次赋值,把其中交换操作改为赋值操作

public static void sort(Comparable[] arr){
     
	if (arr == null || arr.length < 2) {
     
		return;
	}
    
	for (int i = 0; i < arr.length; i++) {
     
		//先把要比较的元素复制一份
		Comparable e = arr[i];
		//从后向前遍历,遇到比e大的就把元素向后移动
		int j = i;
		for (;j > 0 && arr[j-1].compareTo(e) > 0; j--) {
     
			arr[j] = arr[j-1];
		}
		//在停止的位置处赋值e,这样内层循环就只有一次赋值操作
		arr[j] = e;
	}
}

希尔排序

* 希尔排序
*  希尔排序是对插入排序的改进,交换的是不相邻的元素以对数组的局部进行排序,并最终用插入排序将局部有序的数组排序。
*  希尔排序先使数组中任意间隔为h的元素都是有序的,这样的数组被称为h有序数组(一个h有序数组即一个由h个有序子数组组成的数组),
*  在进行排序时,如果h很大,就能将元素移动到很远的地方,为实现更小的h有序创造方便。希尔排序又称“缩小增量排序”,
*  对于每个h,用插入排序将h个子数组独立地排序,只需要在插入排序的代码中将移动元素的距离由1改为h即可,
*  这样希尔排序的实现就转化为一个类似于插入排序但使用不同增量的过程。
* 希尔排序的执行时间依赖于增量序列,希尔增量时间复杂度为O(N^2),Hibbard增量的希尔排序时间复杂度为O(N的1.5次方),
* 下界为N*log2N,不稳定的排序算法。
public static void sort(Comparable[] arr){
     
    int N = arr.length;
    int h = 1;
    //根据数组的长度设定间隔,这里间隔是按照数学计算出来的公式设定的,使其能够达到最优。
    while (h < N/3){
     
        h = 3 * h + 1;
    }
    //以间隔h进行插入排序,这里用大于等于1进行判断,效率要快很多
    while (h >= 1){
     
        //以间隔h进行插入排序
        for (int i = h; i < N; i++) {
     
            for (int j = i;j >= h &&(arr[j].compareTo(arr[j-h]) < 0) ; j -=h) {
     
                swap(arr,j,j-h);
            }
        }
        h/=3;
    }
}

归并排序

*  首先对于一个大的数据集,我们不好排序,我们将这个数据集一分为二,分成两个小的数据集,我们假设,如果这两个 *  数据集排序完成,剩下的工作就是讲这两个有序数据集合并成一个有序的的数据集,于是我们的当下任务可以分成两   *  个,首先第一个是,排序这个两个小的数据集,然后的任务是,将这两个小的数据集整合到一个有序的数据集。
* 
   解决第一个任务:拆分
*
* 如果原来的数据集很大,我们拆分成两个数据集的时候,发现,还是不好排序,怎么办, 答案很简单,说明它还不够   *  小,于是,我们对着两个数据集,也采用相同的做法,在拆分,以此类推,一直到我们的拆分的数据集只有一个数据为 *  止,这个时候,我们发现,对于只有 一个数据元素的数据集,它的顺序也就已经确定了,我们的排序也就完成了。也 *  就是说只要我们对数据不停的拆分,最后将每一个数据集拆分成只有一个数据元素的时候,其排序过程其实就已经完成    了 。   

* 解决第二个任务:合并

* 经过上一个步骤,我们就得到了两个有序的数据集合,然后,我们的任务就是将这两个有序的数据集
* 合并成一个有序的数据集合。这里我们举一个例子,桌子上有两副有序的牌,我们要将他们合并成一副
* 有序的牌,我们该怎么做呢,很简单,假设两副陪得顺序都是从小到大排放,小的在上面,首先,我们
* 分别从两副牌的顶端,各取一个,然后比较谁打谁小,将小的放到手中,大的不动,然后在从两副牌的
* 顶端在取两张牌,再比,以此类推,知道一副牌比完,或者两副牌都同时比完,如果其中的一副牌先比
* 完,则将剩下的牌就不用比了,直接放到手中就行了,这时,手中的牌就是合并好的有序的牌了。

自顶向下

// 声明一个辅助数组
private static Comparable[] aux;

//归并排序的算法
public static void sort(Comparable[] arr){
     
    //因为辅助数组要一直用,因为定义为类属性,便不用每次在递归方法中定义了
    aux = new Comparable[arr.length];
    mergeSort(arr,0,arr.length-1);
}

//具体的算法实现,需要先拆分再归并
public static void mergeSort(Comparable[] arr,int left,int r){
     
    if (left >= r){
     
        return;
    }
    //不断进行拆分
    int mid = left + (r - left)/2;
    mergeSort(arr,left,mid);			//左半边排序
    mergeSort(arr,mid+1,r);				//右半边排序
    merge(arr,left,mid,r);				//归并结果
}

// 将arr[left...mid]和arr[mid+1...r]两部分进行归并
private static void merge(Comparable[] arr, int left, int mid, int r) {
     
    int i = left,j = mid+1;
    //先把两部分的数据都复制到一个数组中
    for (int k = left; k <= r; k++) {
     
        aux[k] = arr[k];
    }
    //在辅助数组中以i,j进行遍历,依据大小进行赋值操作修改原数组
    for (int k = left; k <= r; k++) {
     
        if (i > mid)   					 arr[k] = aux[j++]; //左半边用尽(取右半边元素)
        else if (j > r )				 arr[k] = aux[i++]; //右半边用尽(取左半边元素)
        else if (aux[i].compareTo(aux[j]) > 0)		
             arr[k] = aux[j++];		//右半边的当前元素小于左半边的当前元素(取右半边元素)
        else arr[k] = aux[i++];		//左半边的当前元素小于右半边的当前元素(取左半边元素)					
    }
}

自底向上

//使用自底向上的归并只不过是把递归换成for循环
public static void sort(Comparable[] arr){
     
    int N = arr.length;
    aux = new Comparable[N];
    for (int size = 1; size < N; size += size) {
     
        //每次从左向右以size为间隔进行归并的,因此left每次移动的是2倍的size
        for (int i = 0; i < N-size; i += size+size) {
     
            //对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并,若为奇数则可能会超出数组索引,因此取最小值
            merge(arr,i,i+size-1,Math.min(i+size+size-1,N-1));
        }
    }
}

// 将arr[left...mid]和arr[mid+1...r]两部分进行归并
private static void merge(Comparable[] arr, int left, int mid, int r) {
     
    int i = left,j = mid+1;
    //先把两部分的数据都复制到一个数组中
    for (int k = left; k <= r; k++) {
     
        aux[k] = arr[k];
    }
    //在辅助数组中以i,j进行遍历,依据大小进行赋值操作修改原数组
    //在辅助数组中以i,j进行遍历,依据大小进行赋值操作修改原数组
    for (int k = left; k <= r; k++) {
     
        if (i > mid)   					 arr[k] = aux[j++]; //左半边用尽(取右半边元素)
        else if (j > r )				 arr[k] = aux[i++]; //右半边用尽(取左半边元素)
        else if (aux[i].compareTo(aux[j]) > 0)		
             arr[k] = aux[j++];		//右半边的当前元素小于左半边的当前元素(取右半边元素)
        else arr[k] = aux[i++];		//左半边的当前元素小于右半边的当前元素(取左半边元素)
    }
}

两种实现方法对比:

​ 对于自顶向下的充分的利用了数组检索特性,对于自底向上的则没有利用数组位置检索则对于底层为链表结构的排序速度更快。

快速排序

* 快速排序(Quick Sort)是对冒泡排序的一种改进,基本思想是选取一个记录作为枢轴,
* 经过一趟排序,将整段序列分为两个部分,其中一部分的值都小于枢轴,另一部分都大于枢轴。
* 然后继续对这两部分继续进行排序,从而使整个序列达到有序。

经典实现

public static void sort(Comparable[] arr){
     
    if (arr == null || arr.length <= 1){
     
        return;
    }
    sort(arr,0,arr.length-1);
}

// 对arr[left...r]部分进行partition操作
// 返回p, 使得arr[left...p-1] < arr[p] ; arr[p+1...r] >= arr[p]
private static int partition(Comparable[] arr,int left,int r){
     
    Comparable v = arr[left];
    int j = left;
    for (int i = left+1; i <= r; i++) {
     
        if (arr[i].compareTo(v) < 0){
     
            //应该与大于等于v的第一个元素交换位置
            j++;
            swap(arr,j,i);
        }
    }
    //把基准元素放中间
    swap(arr,left,j);

    return j;
}

//使用分治的方法进行拆分
private static void sort(Comparable[] arr, int left, int r) {
     
    if (left >= r){
     
        return;
    }
    int p = partition(arr,left,r);
    //p其实已经有序了,不用再考虑了
    sort(arr,left,p-1);
    sort(arr,p+1,r);
}

双路快排

重点在partion部分的改进

public static void sort(Comparable[] arr){
     
    if (arr == null || arr.length <= 1){
     
        return;
    }
    sort(arr,0,arr.length-1);
}

//双路排序的拆分
private static int partition2(Comparable[] arr,int left,int r){
     
    Comparable v = arr[left];
    int i = left+1,j = r;
    while (true){
     
        //这里有一个地方要注意的就是不要添加等号,以为加上等号就可以少交换一次,但正是因为少交换了一次,就可能会造成拆分的不平衡
        //这样做的后果是效率反而低了
        while (i <= r && arr[i].compareTo(v) < 0){
     
            i++;
        }
        while (j > left && arr[j].compareTo(v) > 0){
     
            j--;
        }
        if (i > j){
     
            break;
        }
        //交换i与j位置处的元素值
        swap(arr,i,j);
        i++;
        j--;
    }
    //把基准元素放入合适的位置
    swap(arr,left,j);
    return j;
}

//使用分治的方法进行拆分
private static void sort(Comparable[] arr, int left, int r) {
     
    if (left >= r){
     
        return;
    }
    int p = partition(arr,left,r);
    //p其实已经有序了,不用再考虑了
    sort(arr,left,p-1);
    sort(arr,p+1,r);
}

三路快排

public static void sort(Comparable[] arr){
     
    if (arr == null || arr.length <= 1){
     
        return;
    }
    sort(arr,0,arr.length-1);
}

private static void partition3(Comparable[] arr,int left,int r){
     
    if(r <= left){
     
        return;
    }
    Comparable v = arr[left];
    //arr[left+1...lt-1] < v  arr[lt...i) == v   arr[gt...r] > v
    int lt = left,i = left+1,gt = r;
    while (i <= gt){
     
        int cmp = arr[i].compareTo(v);
        if (cmp < 0){
     
            swap(arr,lt++,i++);
        }else if (cmp > 0){
     
            //因为gt位置处的值并不确定,因此换过来后i的值不自增,需要再对i处的值判断一次
            swap(arr,i,gt--);
        }else {
     
            i++;
        }
    }
    partition3(arr,left,lt-1);
    partition3(arr,gt+1,r);
}

//使用分治的方法进行拆分
private static void sort(Comparable[] arr, int left, int r) {
     
    if (left >= r){
     
        return;
    }
    int p = partition(arr,left,r);
    //p其实已经有序了,不用再考虑了
    sort(arr,left,p-1);
    sort(arr,p+1,r);
}

堆排序这个好像不是重点

交换方法

//采用异或能够提高运行效率
public static void swap(int[] arr, int i, int j) {
     
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
}

单例模式

1.饿汉式单例模式

public class Singleton {

    //类初始化时,立即加载这个对象,加载类时,天然是线程安全的
    private static Singleton singleton = new Singleton();    //类初始化时,立即加载这个对象

    private Singleton(){}  //私有化构造器

    //方法没有同步,效率高
    public static Singleton getInstance(){
        return singleton;
    }
}

2.懒汉式单例模式

public class Singleton implements Serializable {

    //类初始化时,不初始化这个对象(延时加载)
    private static Singleton singlet;

    private Singleton() {
        if(singlet != null){        //防止反射破解
            throw new RuntimeException();
        }
    }    //私有化构造器

    //方法同步,调用效率高!
    public static synchronized Singleton getInstance(){  //不加synchronized非线程安全
        if(singlet == null){
            singlet = new Singleton();
        }
        return singlet;
    }

    //反序列化时,如果定义了readResolve方法则直接返回方法指定的对象,则不需要单独再创建新对象
    private Object readResolve() throws ObjectStreamException{
        return singlet;
    }
}

3.懒汉式(双重检查加锁版本)

public class Singleton {

    //volatile保证,当uniqueInstance变量被初始化成Singleton实例时,多个线程可以正确处理uniqueInstance变量
    private volatile static Singleton uniqueInstance;
    private Singleton() {
    }
    public static Singleton getInstance() {
       //检查实例,如果不存在,就进入同步代码块
        if (uniqueInstance == null) {
            //只有第一次才彻底执行这里的代码
            synchronized(Singleton.class) {
               //进入同步代码块后,再检查一次,如果仍是null,才创建实例
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

4.内部类单例模式

要点:
*          外部类没有static属性,不会像饿汉式那样立即加载
*          只有真正调用getInstance(),才会加载静态内部类,加载类时时线程安全的。
*          instance 是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,
*          从而保证了线程安全性,兼顾了并发高效调用和延迟加载的优势!
**/
public class Singleton {
     

    private static class SingletonClassInstance{
     
        private static final Singleton instance = new Singleton();      //static final
    }

    private Singleton(){
     }

    public static Singleton getInstance(){
     
        return SingletonClassInstance.instance;
    }
}

5.枚举

public class Singleton {
     
    public static void main(String[] args) {
     
        Single single = Single.SINGLE;
        single.print();
    }

  public  enum Single {
     
        SINGLE;     
        private Single() {
     
        }
      
        public void print() {
     
            System.out.println("hello world");
        }
      
    }
}

LRU

labuladong的算法小抄+LeetCode

算法描述

​ 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。

​ 为了使删除操作时间复杂度为 O(1),就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。

算法实现

双向链表+HashMap

基础定义

//节点定义
class Node {
     
	public int key,val;
    public Node next,prev;
    
    public Node(int k,int v){
     
        this.key = k;
        this.val = v;
    }
}

class DoubleList{
     
    
    //在链表头部添加节点x,时间复杂度O(1)
    public void addFirst(Node x);
    
    //删除链表的x节点,时间复杂度O(1)
   	public void remove(Node x);
    
    //删除链表的最后一个节点,时间复杂度O(1)
    public Node removeLast();
    
    //返回链表长度,时间复杂度O(1)
    public int size();
}

LRU实现

class LRUCache{
     
    //定义key到节点的映射,map中保存的是节点
	private HashMap<Integer,Node> map;	
    private DoubleList cache;
    //最大容量
    private int cap;
    
    public LRUCache(int capacity){
     
        this.cap = capacity;
        map = new HashMap<>();
        cache = new DoubleList();
    }
    
    public int get(int key){
     
        if(!map.containsKey(key))
            return -1;
        int val = map.get(key).val;
        //利用put方法把该方法提前
        put(key,val);
        return val;
    }
    
    public void put(int key, int val){
     
        //先把新节点x做出来
        Node x = new Node(key,val);
        
        if(map.containsKey(key)){
     
            //删除旧的节点,新的插到头部
            cache.remove(map.get(key));
            cache.addFirst(x);
            //更新map中对应的数据
            map.put(key,x);
        }else{
     
            //缓存容量满时删除最后一个节点
            if(cap == cache.size()){
     
                Node last = cache.removeLast();
                map.remove(last.key);
            }
            //直接添加到头部
            cache.addFirst(x);
            map.put(key,x);
        }
    }
}

注:

​ 链表中要同时保存key和val,因为当缓存容量满时,不仅仅要删除最后一个节点,还要在map中进行删除,如果只在链表中保存val值,那么就无法在map集合中找到要删除的key了。

在Java中可以调用LinkedHashMap来完成上述操作

//继承LinkedHashMap
class LRUCache extends LinkedHashMap<Integer, Integer>{
     
    
    private int capacity;
    
    public LRUCache(int capacity) {
     
        //调用父类构造方法
        super(capacity, 0.75F, true);
        this.capacity = capacity;
    }

    public int get(int key) {
     
        return super.getOrDefault(key, -1);
    }

    // 这个可不写,因为父类中已经有了该方法
    public void put(int key, int value) {
     
        super.put(key, value);
    }
	
    //重写父类的规则
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
     
        return size() > capacity; 
    }
}

---------------------原创文章, 转载请注明出处---------------------------
---------------------原创文章, 转载请注明出处---------------------------
---------------------原创文章, 转载请注明出处---------------------------

你可能感兴趣的:(java,C++,算法,数据结构,java,排序算法,动态规划)