JAVA 算法

在这里插入图片描述

5遍刷题法
刷题第一遍
·5~10分钟:读题+思考
·有思路,可以上手实现,没有思路怎么办?可以直接看解法,这个没有任何问题,但是注意因为一个题目可能有多种解法,比较解法优劣后,背诵、默写好的解法。有自己的思路,依然可以去看别人的解法,在比较解法优劣,找到差距后,背诵、默写。背诵、默写很重要,是你理解的基础。同时没有必要在一个题目上花费很多时间,比如一天、两天去琢磨一个题目的实现。
刷题第二遍
马上自己写,这就是不看别人的解法,自己动手写,写完后提交到LeetCode运行,依然要去看看自己的实现和别人解法的差距,并思考差距在什么地方,比如你的解法明明和别人的思路一样,但是速度却很不理想,就要看看为什么我的实现要慢,并且着手优化,争取要让你的解法领先80%的人,当然越高越好。
刷题第三遍
·过了24小时后,再重复做题
刷题第四遍
过了一周:反复回来练习相同题目
刷题第五遍
面试前一周或半个月专门性进行恢复性训练。并且在面试前强烈推荐大家在纸上和黑板上练习写代码,这对书写清晰可读的代码是非常有帮助的。

part1

算法时间复杂度分析

只关注循环执行次数最多的一段代码
总复杂度等于最高阶项的复杂度
嵌套代码的复杂度等于嵌套内外代码复杂度的乘积

推导大O阶:
1.用常数1取代运行时间中的所有加法常数。
2.在修改后的运行次数函数中,只保留最高阶项。
3.如果最高阶项存在且不是1,则去除与这个项相乘的常数。得到的结果就是大O阶。

常见时间复杂度
虽然代码千差万别,但是常见的复杂度量级并不多。大致包括:
O(1) 常数阶
O(n) 线性阶
O(n2) 平方阶
O(logn) 对数阶
O(nlogn ) 线性对数阶
O(n3) 立方阶
O(2n) 指数阶
O(n!) 阶乘阶
从小到大依次是:
O(1)<O(logn)<O(n)< O(nlogn)< O(n2)<O(n3)<O(2n)<O(n!)<O(nn)

递归

1.一个问题的解可以分解为几个子问题的解。
2.这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。
3.存在基线/终止条件。

爬楼梯

在这里插入图片描述
在这里插入图片描述

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.1 阶 + 1 阶
2.2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.1 阶 + 1 阶 + 1 阶
2.1 阶 + 2 阶
3.2 阶 + 1 阶

public static int climbStarts(int num){
	if(num==1) return 1;
	if(num==2) return 2;
	
	return climbStarts(num-2)+climbStarts(num-1);

//        if (num==1) {
//            return 1;
//        }
//        if (num==2){
//            return 2;
//        }
//        int result = 0;
//        int pre = 2;
//        int prePre = 1;
//        for (int i = 3; i <= num; i++) {
//            result=pre+prePre;
//            prePre=pre;
//            pre=result;
//        }
//        return result;
}

斐波那契数列

写一个函数,输入n,求出斐波那契数列的第n项。斐波那契数列的定义如下:

1,1,2,3,5,8,13,21,34

public static int fibonacciRecursive(int num ){
        if (num==2) {
            return 1;
        }
        if (num==1){
            return 1;
        }
        return fibonacciRecursive(num-1)+fibonacciRecursive(num-2);
    }

两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。
你可以按任意顺序返回答案。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]

//暴力穷举,复杂度O(n2)。
public static int [] twoSum(int target, int [] nums){
        int [] arrays = new int[2];
        for (int i = 0; i < nums.length; i++) {
            for (int j =i+1;i < nums.length;i++){
                if (nums[i]+nums[j]==target){
                    arrays[0]=i;
                    arrays[1]=j;
                    return arrays;
                }
            }
        }
        return arrays;
    }

合并两个有序数组

给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]
解释:需要合并 [1,2,3] 和 [2,5,6] 。
合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0
输出:[1]
解释:需要合并 [1] 和 [] 。
合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1
输出:[1]
解释:需要合并的数组是 [] 和 [1] 。
合并结果是 [1] 。
注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。

    public static int [] mergeArray(int[] nums1,int m,int[]nums2,int n){
        for (int i = 0; i < nums2.length; i++) {
            nums1[m+i]=nums2[i];
        }
        Arrays.sort(nums1);
        return nums1;
    }
    
	public static int [] mergeArray2(int [] nums1,int m,int [] nums2,int n){
        int j =0;
        for (int i = m; i < nums1.length; i++) {
            nums1[i] = nums2[j];
            j++;
        }
        Arrays.sort(nums1);
        return nums1;
    }
    public static int [] mergeArray3(int [] nums1,int m,int [] nums2,int n){
        int j=0;
        for (int i = 0; i < n; i++) {
            nums1[m+i]=nums2[i];
        }
        Arrays.sort(nums1);
        return nums1;
    }

移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
示例:
输入: [0,1,0,3,12]
输出: [1,3,12,0,0]
说明:
必须在原数组上操作,不能拷贝额外的数组。
尽量减少操作次数。

public static int [] moveZeroIndex(int[] nums){
        if (nums == null) {
            return null;
        }
        int j =0;
        for (int i = 0; i < nums.length; i++) {
            if (nums[i]!=0){
                nums[j++]=nums[i];
            }
        }
        for (int i = j; i < nums.length; i++) {
            nums[i]=0;
        }
        return nums;
    }

找到所有数组中消失的数字

给你一个含 n 个整数的数组 nums ,其中 nums[i] 在区间 [1, n] 内。请你找出所有在 [1, n] 范围内但没有出现在 nums 中的数字,并以数组的形式返回结果。
示例 1:
输入:nums = [4,3,2,7,8,2,3,1]
输出:[5,6]
示例 2:
输入:nums = [1,1]
输出:[2]
提示:
n == nums.length
1 <= n <= 105
1 <= nums[i] <= n

public static Integer [] lostNum(/*Integer[] nums ,*/int [] nums){
        Arrays.sort(nums);
//        List<Integer> list = new ArrayList<Integer>();
//        for (int num : nums) {
//            list.add(num);
//        }
//        List<Integer> list = Arrays.asList(nums);
        
        Integer[] integers = Arrays.stream(nums).boxed().toArray(Integer[]::new);
        List<Integer> list = Arrays.asList(integers);
        List<Integer> lostlist = new ArrayList<Integer>();
        for (int i = 1; i <= nums.length; i++) {
            if (!list.contains(i)) {
                lostlist.add(i);
            }
        }
        Integer [] arrays =new Integer[lostlist.size()];
//        for (int i = 0; i < lostlist.size(); i++) {
//            arrays[i]=lostlist.get(i);
//        }

//        return arrays;
//        return lostlist.toArray(arrays);
		return lostlist.stream().mapToInt(Integer::intValue).toArray();
    }

boxed()
用于将原始类型的流(如IntStream、LongStream、DoubleStream)转换为其对应的包装类型的流(如Stream、Stream、Stream)

mapToInt方法的主要作用是将对象流中的每个元素映射到一个int值,创建一个IntStream。该方法接受一个ToIntFunction函数,这个函数定义了如何将流中的每个元素转换为int。

合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
示例 2:
输入:l1 = [], l2 = []
输出:[]
示例 3:
输入:l1 = [], l2 = [0]
输出:[0]
提示:
两个链表的节点数目范围是 [0, 50]
-100 <= Node.val <= 100
l1 和 l2 均按 非递减顺序 排列

在这里插入图片描述

class ListNode{
        ListNode pre;
        ListNode next;
        Integer val;

       //set,get忽略
    }

    public static ListNode mergeLists(ListNode l1,ListNode l2){
        if (l1 == null) return l1;
        if (l2 == null) return l2;
        if (l1.val<l2.val){
            l1.next=mergeLists(l1,l2.next);
            return l1;
        }
        l2.next=mergeLists(l1.next,l2);
        return l2;
    }

删除排序链表中的重复元素

利用hash

class ListNode {
    int val;
    ListNode next;
    ListNode(int x) { val = x; }
}

public static ListNode deleteDuplicates(ListNode head) {
    if (head == null || head.next == null) {
        return head;
    }

    HashSet<Integer> seen = new HashSet<>();
    ListNode current = head;
    ListNode prev = null;

    while (current != null) {
        if (seen.contains(current.val)) {
            // 如果当前值已在哈希表中,则跳过当前节点
            prev.next = current.next;
        } else {
            // 否则,将当前值加入哈希表,并继续处理下一个节点
            seen.add(current.val);
            prev = current;
        }
        current = current.next;
    }

    return head;
}

public static void printList(ListNode head) {
        ListNode current=head;
        while (current!=null){
            System.out.println(current.val);
            current=current.next;
        }
    }

环形链表1

给定个链表,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

在这里插入图片描述

public boolean hasCycle(ListNode root){
        if (root == null) {
            return false;
        }
        ListNode fast=root;
        ListNode slow=root;
        while (fast.next!=null&&fast.next.next!=null){
            fast=fast.next.next;
            slow=slow.next;
            if (fast==slow) {
                return true;
            }
        }
        return false;
    }

环形链表2

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
在这里插入图片描述

    public ListNode delectCycle(ListNode head){
        if (head == null) {
            return null;
        }
        ListNode fast=head;
        ListNode slow=head;
        boolean flag= false;
        while (fast.next != null && fast.next.next != null) {
            slow=slow.next;
            fast=fast.next.next;
            if (slow==fast) {
                flag=true;
                break;
            }
        }
        if (flag) {//存在环
            slow=head;
            while (slow != fast) {
                slow=slow.next;
                fast=fast.next;
            }
            return slow;//返回环存在的开始节点
        }
        return null;
    }

相交链表

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。

在这里插入图片描述

	public ListNode getIntersectionNode(ListNode rootA,ListNode rootB){
        if (rootA == null || rootB == null ) {
            return null;
        }
        ListNode headA=rootA;
        ListNode headB=rootB;
        while (headA != headB) {
            headA=headA==null?headB:headA.next;
            headB=headB==null?headA:headB.next;
        }
        return headA;
    }

反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
在这里插入图片描述
其他思路:hash 然后倒过来遍历

public ListNode reverseListNode(ListNode root){
        ListNode current =root;
        ListNode pre=null;
        while (current != null) {
            ListNode next= current.next;
            current.next=pre;
            pre=current;
            current=next;
        }
        return pre;
    }

回文链表

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false 。
在这里插入图片描述

	public boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        
        // 找到链表的中间节点(快慢指针法)
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        
        // 反转链表的后半部分
        ListNode secondHalf = reverseList(slow);
        ListNode firstHalf = head;
        
        // 比较两个部分的值
        while (secondHalf != null) {
            if (firstHalf.val != secondHalf.val) {
                return false;
            }
            firstHalf = firstHalf.next;
            secondHalf = secondHalf.next;
        }
        
        return true;
    }
    
    // 反转链表
    private ListNode reverseList(ListNode head) {
        ListNode prev = null;
        while (head != null) {
            ListNode nextNode = head.next;
            head.next = prev;
            prev = head;
            head = nextNode;
        }
        return prev;
    }

链表的中间结点

给定一个头结点为 head 的非空单链表,返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。

思路:循环先计算有几个节点,然后取模是否为0,不为0加1,然后再循环得到对应的节点

链表中倒数第k个节点

思路:1.hashMap
2.反转链表,然后循环

栈与队列

栈(堆栈)

枪都有弹夹,弹夹里都有弹簧,在开枪的时候,先压入弹夹的子弹反而是最后才能射出来的。
在这里插入图片描述

栈就是像弹夹一样的数据结构,先进去的元素,却要后出来,而后进的元素,反而可以先出来。栈有时也被称为堆栈,注意要和堆进行区分。(先进后出)

队列

去买东西,收银窗口很少,而付款的人很多,于是就在收银窗口前排起队来,先排在收银窗口的人自然就先付款先离开,后排队的人后付款后离开。
这就是队列的概念,队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出 (First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
在这里插入图片描述

用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:
void push(int x) 将元素 x 推到队列的末尾
int pop() 从队列的开头移除并返回元素
int peek() 返回队列开头的元素
boolean empty() 如果队列为空,返回 true ;否则,返回 false

在这里插入图片描述

public class MyQueue {
    private static Stack<Integer> inStack;
    private static Stack<Integer> outStack;
    public MyQueue() {
        inStack=new Stack<>();
        outStack=new Stack<>();
    }
    public void push(int x){
        inStack.push(x);
    }
    public int pop(){
        if (outStack.empty()) {
            inToOut();
        }
        return outStack.pop();
    }
    public int peek(){
        if (outStack.empty()) {
            inToOut();
        }
        return outStack.peek();
    }
    public boolean empty(){
        return inStack.empty() && outStack.empty();
    }
    private void inToOut(){
        while (!inStack.empty()) {
            outStack.push(inStack.pop());
        }
    }
}

字符串解码

给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
示例 1:
输入:s = “3[a]2[bc]”
输出:“aaabcbc”
示例 2:
输入:s = “3[a2[c]]”
输出:“accaccacc”
示例 3:
输入:s = “2[abc]3[cd]ef”
输出:“abcabccdcdcdef”
示例 4:
输入:s = “abc3[cd]xyz”
输出:“abccdcdcdxyz”

在这里插入图片描述
在这里插入图片描述

元素之间是相邻的一对一的关系。然后对这条线上的元素的访问,就是顺着这条线移动,所以链表或者数组也被称为线性表。
可现实中,还有很多一对多的情况需要处理,所以需要研究这种一对多的数据结构——“树”。

树的定义是:

树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:
(1)有且仅有一个特定的称为根(Root)的结点;
(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree )。
在这里插入图片描述
在上图中,A结点就是根结点,子树T1和子树T2就是根结点A的子树。当然,D、G、H、I组成的树又是B为根结点的子树,E、J组成的树是C为根结点的子树。
但是要注意:
1、根结点是唯一的,不可能存在多个根结点,别和现实中的大树混在一起,现实中的树有很多根须,那是真实的树,数据结构中的树是只能有一个根结点。
2. 子树的个数没有限制但它们一定是互不相交的。下面的两个结构就不符合树的定义,因为它们都有相交的子树。

二叉树的相关概念

二叉树属于一种特殊的树,定义如下:
二叉树(Binary Tree)是n(n≥0)个结点的有限集合,该集合或者为空集(称为空二叉树)。或者由一个根结点和两棵互不相交的、分别称为根结点的左子树和右子树的二叉树组成。
在这里插入图片描述
二叉树的特点有:
每个结点最多有两棵子树,所以二叉树中不存在度大于2的结点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
.左子树和右子树是有顺序的,次序不能任意颠倒。就像人是双手、双脚,但显然左手、左脚和右手、右脚是不一样的,右手戴左手套、右脚穿左鞋都会极其别扭和难受。
即使树中某结点只有一棵子树,也要区分它是左子树还是右子树。如下图,都是二叉树,但它们却是不同的二叉树。

特殊二叉树

斜树(类似于链表)
顾名思义,斜树一定要是斜的,但是往哪斜还是有讲究。所有的结点都只有左子树的二叉树叫左斜树。所有结点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。斜树有很明显的特点,就是每一层都只有一个结点,结点的个数与二叉树的深度相同。

满二叉树
在一棵二叉树中,如果所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树。
单是每个结点都存在左右子树,不能算是满二叉树,还必须要所有的叶子都在同一层上,这就做到了整棵树的平衡。
在这里插入图片描述
因此,满二叉树的特点有:
(1)叶子只能出现在最下一层。出现在其他层就不可能达成平衡。
(2)非叶子结点的度一定是2。否则就是“缺胳膊少腿”了。
(3)在同样深度的二叉树中,满二叉树的结点个数最多,叶子数最多。
(只有叶子节点有数据,非叶子节点都是满‘’)

完全二叉树
对一棵具有n个结点的二叉树按层序编号,如果编号为i (1<i<n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。
在这里插入图片描述
首先从字面上要区分,“完全”和“满”的差异,满二叉树一定是一棵完全二叉树,但完全二叉树不一定是满的。
其次,完全二叉树的所有结点与同样深度的满二叉树,它们按层序编号相同的结点,是一一对应的。这里有个关键词是按层序编号。

二叉树的遍历

二叉树的中序遍历

给定一个二叉树的根节点 root ,返回它的 中序 遍历。
在这里插入图片描述
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
进阶: 递归算法很简单,你可以通过迭代算法完成吗?

//递归方式
    public List<Integer> inorderMidTree(TreeNode root){
        ArrayList<Integer> list = new ArrayList<>();
        accessTree(root,list);
        return list;
    }

    public void accessTree(TreeNode root,List<Integer> list){
        if (root == null) {
            return;
        }
        accessTree(root.left,list);
        list.add(root.val);
        accessTree(root.right,list);
    }
//循环迭代方式
	public List<Integer> inorderTraversal(TreeNode root){
        List<Integer> list = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        while (root != null || stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root=root.left;
            }
            root=stack.pop();
            list.add(root.val);
            root=root.right;
        }
        return list;
    }

二叉树的前序遍历

给你二叉树的根节点 root ,返回它节点值的 前序 遍历。
示例 1:
在这里插入图片描述
输入:root = [1,null,2,3]
输出:[1,2,3]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]

//递归方式
    public List<Integer> preInorderTree(TreeNode root){
        ArrayList<Integer> list = new ArrayList<>();
        preAccessTree(root,list);
        return list;
    }
    private void preAccessTree(TreeNode root, ArrayList<Integer> list) {
        if (root == null) {
            return;
        }
        list.add(root.val);
        preAccessTree(root.left,list);
        preAccessTree(root.right,list);
    }
//循环迭代方式
    public List<Integer> preInorderTraversal(TreeNode root){
        ArrayList<Integer> list = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        while (root != null || stack.isEmpty()) {
            while (root != null) {
                list.add(root.val);
                stack.push(root);
                root=root.left;
            }
            root = stack.pop();
            root = root.right;
        }
        return list;
    }

二叉树的后序遍历

//递归方式
public List<Integer> postInorderTree(TreeNode root){
        ArrayList<Integer> list = new ArrayList<>();
        postAccessTree(root,list);
        return list;
    }

    private void postAccessTree(TreeNode root, ArrayList<Integer> list) {
        if (root == null) {
            return;
        }
        postAccessTree(root.left,list);
        postAccessTree(root.right,list);
        list.add(root.val);
    }
//循环迭代方式
	public List<Integer> postInorderTraversal(TreeNode root){
        ArrayList<Integer> list = new ArrayList<>();
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode prevNode = null;
        while (root != null || stack.isEmpty()) {
            while (root!=null){
                stack.push(root);
                root=root.left;
            }
            root = stack.pop();
            if (root==null || root==prevNode){
                list.add(root.val);
                prevNode=root;
                root=null;
            }else {
                stack.push(root);
                root=root.right;
            }
        }
        return list;
    }

对称二叉树

题目
给定一个二叉树,检查它是否是镜像对称的。
在这里插入图片描述

//递归方式
	public boolean isSymmetric(TreeNode root){
        if (root == null) {
            return true;
        }
        return deepCheck(root.left, root.right);
    }

    private boolean deepCheck(TreeNode left, TreeNode right) {
        if (left==null&&right==null){
            return true;
        }
        if (left==null||right==null){
            return false;
        }
        if (left.val!=right.val) {
            return false;
        }
        return deepCheck(left.left,right.right)&&deepCheck(left.right,right.left);
    }

//循环迭代方式
	public boolean synmmetric(TreeNode root){
        Queue<TreeNode> queue = new LinkedList<>();
        TreeNode left = root.left;
        TreeNode right = root.right;
        if (root==null||(left==null&&right==null)) {
            return true;
        }
        queue.offer(left);
        queue.offer(right);
        while (!queue.isEmpty()) {
            left=queue.poll();
            right=queue.poll();
            if (left==null&&right==null) {
                continue;
            }
            if ((left==null||right==null)||(left.val!=right.val)){
                return false;
            }
            queue.offer(left.left);
            queue.offer(right.right);

            queue.offer(left.right);
            queue.offer(right.left);
        }
        return true;
    }

二叉树的最大深度

给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

在这里插入图片描述

//递归方式
	public int maxDepth(TreeNode root){
        if (root == null) {
            return 0;
        }
        return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;
    }
//循环迭代方式
	public int maxDepthWithQueue(TreeNode root){
        if (root == null) {
            return 0;
        }
        int depth = 0;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size>0){
                TreeNode node = queue.poll();
                if (node.left!=null) {
                    queue.offer(node.left);
                }
                if (node.right!=null) {
                    queue.offer(node.right);
                }
                size--;
            }
            depth++;
        }
        return depth;
    }

平衡二叉树

给定一个二叉树,判断它是否是高度平衡的二叉树。本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
在这里插入图片描述
在这里插入图片描述

public boolean isBalanced(TreeNode root){
        if (root == null) {
            return true;
        }
        return helper(root)!=-1;
    }

    private int helper(TreeNode root) {
 	    if (root == null) {
            return 0;
        }
        int left = helper(root.left);
        int right = helper(root.right);
        if (left==-1||right==-1||Math.abs(left-right)>1){
            return -1;
        }
        return Math.max(left,right)+1;
    }

翻转二叉树

翻转一棵二叉树。
在这里插入图片描述

	public TreeNode reverTree(TreeNode root){
        if (root == null) {
            return null;
        }
        reverTree(root.left);
        reverTree(root.right);
        TreeNode temp = root.left;
        root.left = root.right;
        root.right = temp;
        return root;
    }

排序算法

排序就是将一组对象按照某种逻辑顺序重新排列的过程。比如,订单按照日期排序的——这种排序很可能使用了某种排序算法。在计算时代早期,大家普遍认为30% 的计算周期都用在了排序上。如果今天这个比例降低了,可能的原因之一是如今的排序算法更加高效,而并非排序的重要性降低了。现在计算机的广泛使用使得数据无处不在,而整理数据的第一步通常就是进行排序。几乎所有的计算机系统都实现了各种排序算法以供系统和用户使用。
即使你只是使用标准库中的排序函数,学习排序算法仍然有三大实际意义:

  • IT从业人员必备技能,也是互联网公司面试的必考点;
  • 类似的技术也能有效解决其他类型的问题;
  • 排序算法常常是解决其他问题的第一步。
    排序在商业数据处理和现代科学计算中有着重要的地位,它能够应用于事物处理、组合优化、天体物理学、分子动力学、语言学、基因组学、天气预报和很多其他领域。其中一种排序算法(快速排序)甚至被誉为20 世纪科学和工程领域的十大算法之一。

数据结构和算法中,关于排序有十大算法,包括冒泡排序,简单选择排序,简单插入排序,归并排序,堆排序,快速排序、希尔排序、计数排序,基数排序,桶排序。

一般在面试中最常考的是快速排序和堆排序、归并排序,并且经常有面试官要求现场写出这3种排序的代码。对这3种排序的代码一定要信手拈来才行。对于其他排序可能会要求比较各自的优劣、各种算法的思想及其使用场景,还有要知道算法的时间和空间复杂度。
通常查找和排序算法的考察是面试的开始,如果这些问题回答不好,估计面试官都没有继续面试下去的兴趣都没了。所以想开个好头就要把常见的排序算法思想及其特点要熟练掌握,有必要时要熟练写出代码。将由易到难学习这十种算法。

冒泡排序

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为相关的元素会经由交换慢慢“浮”到数列的顶端。
基本思路:
1、比较相邻的元素。如果第一个比第二个大(小),就交换它们两个;
2、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大(小)的数;
3、针对所有的元素重复以上的步骤,除了最后一个;
重复步骤1~2,直到排序完成。

冒泡升序示例:

第一轮循环:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
后面继续从头开始循环,直到所有的元素都排好位置为止。

	public int [] sortArray(int [] nums){
        if (nums.length==0) {
            return nums;
        }
        for (int i = 0; i < nums.length; i++) {
            for (int j = 0; j < nums.length - 1 - i; j++) {
                if (nums[j]>nums[j+1]){
                    int temp = nums[j + 1];
                    nums[j+1]=nums[j];
                    nums[j]=temp;
                }
            }
        }
        return nums;
    }

选择排序

选择排序的思想其实和冒泡排序有点类似,都是在一次排序后把最小的元素放到最前面。但是过程不同,冒泡排序是通过相邻的比较和交换。而选择排序是通过对整体的选择。
其实选择排序可以看成冒泡排序的优化,因为其目的相同,只是选择排序只有在确定了最大(小)的前提下才进行交换,大大减少了交换的次数。
具体步骤
1.首先,找到数组中最大(小)的那个元素;
2.其次,将它和数组的第一个元素交换位置(如果第一个元素就是最大(小)元素那么它就和自己交换);
3.再次,在剩下的元素中找到最大(小)的元素,将它与数组的第二个元素交换位置。
如此往复,直到将整个数组排序。

	public static int [] selectorArray(int [] nums){
        if (nums == null || nums.length==0) {
            return nums;
        }
        for (int i = 0; i < nums.length; i++) {
            int minIndex = i;
            for (int j = 0; j < nums.length; j++) {
                if(nums[j]<nums[minIndex])
                    minIndex=j;
            }
            int temp = nums[minIndex];
            nums[minIndex]=nums[i];
            nums[i]=temp;
        }
        return nums;
    }
选择排序(升序)示例

原始数组:
在这里插入图片描述
在这里插入图片描述
如此重复,最后形成数组:
在这里插入图片描述

插入排序

插入排序不是通过交换位置而是通过比较找到合适的位置插入元素来达到排序的目的的。相信大家都看过打麻将,在摸牌的时候,拿到一张牌,找到一个合适的位置插入。举个例子,手里有3万,4万,6万,8万这几张牌,收到5万这张牌,把6万,8万往后移,然后把5万放到原理6万的位置,这个原理其实和插入排序是一样的。
具体步骤:

  • 对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
  • 为了给要插入的元素腾出空间,需要将插入位置之后的已排序元素在都向右移动一位。

插入排序所需的时间取决于输入中元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比对随机顺序的数组或是逆序数组进行排序要快得多。
总的来说,插入排序对于部分有序的数组十分高效,也很适合小规模数组。

	public static int [] insertArray(int [] nums){
        if (nums == null||nums.length==0) {
            return nums;
        }
        for (int i = 0; i < nums.length; i++) {
            int j = i;
            int key = nums[i+1];
            while (j>=0&&nums[j]>key){
                nums[j+1]=nums[j];
                j--;
            }
            nums[j+1]=key;
        }
        return nums;
    }
插入排序(降序)示例

原始数组:
在这里插入图片描述
总是认为原始数组的第一个元素已经是有序的了,于是从第二个元素开始进行排序。
在这里插入图片描述
第二个元素是L,比T小,所以将T后移一位,L插入T原来的位置
在这里插入图片描述
第三个元素是M,在已经排序的序列L、T中,L比M小,L不动,继续比较,T比M大,所以将T后移一位,M插入T原来的位置

在这里插入图片描述
第四个元素是A,在已经排序的序列LMT中,A比它们都下,应该排在最前面,所以将LMT全部后移一位,A插入L原来的位置。
在这里插入图片描述
如此重复,最后形成结果数组:
在这里插入图片描述

快速排序

快速排序被誉为20 世纪科学和工程领域的十大算法之一。快速排序(Quicksort)是对冒泡排序的一种改进,也是采用分治法的一个典型的应用。
首先任意选取一个数据(比如数组的第一个数)作为关键数据,我们称为基准数,然后将所有比它小的数都放到它前面,所有比它大的数都放到它后面,这个过程称为一趟快速排序,也称为分区(partition)操作。在实际实现时,一般会在原数组上直接操作。
通过一趟快速排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

快速排序原理

在这里插入图片描述
1、选择数组的第一个数35为基准,切分数组,将所有比35大的都放到35前面,所有比35小的,放到35后面,进行快速排序
在这里插入图片描述
于是数组变成了:
在这里插入图片描述
2、对35左边的子数组,以63为基准数,进行快速排序,于是左边数组变为:

在这里插入图片描述
对35右边的子数组,以9为基准数,进行快速排序,于是左边数组变为:
在这里插入图片描述
3、继续快速排序下去,最终形成有序数组
在这里插入图片描述
以上是快速排序的一个基本原理,看起来仿佛需要额外的数组进行辅助,但其实在实现的时候,并不需要,只要借助一个分割指示器就可以了。

	public static int[] quickArray(int [] nums,int start,int end){
        if (start<end){
            // 找到分区索引
            int partion = partion(nums, start, end);
            // 递归对分区进行排序
            quickArray(nums,start,partion-1);
            quickArray(nums,partion+1,end);
        }
        return nums;
    }

	public static int quickArray(int [] nums,int start,int end){
        if (start==end)return start;
        // 选取数组最后一个元素作为基准值
        int position = nums[end];
        // 初始化左侧子数组的结束索引
        int i =start-1;
        // 遍历数组,将小于基准值的元素交换到左侧
        for (int j = start; j < end; j++) {
            if (nums[j]<position) {
                i++;
                swap(nums,i,j);
            }
        }
        // 将基准值交换到正确的位置
        swap(nums,i+1,end);
        // 返回基准值的索引
        return i+1;
    }
    public static void swap(int [] nums,int i,int j){
        int temp = nums[i];
        nums[i]=nums[j];
        nums[j]=temp;
    }
快速排序(升序)实现图示

在这里插入图片描述
1、随机选择数组的一个数,比如48为基准数(用以进行切分数组), 同时引入一个分区指示器,这个分区指示器初始值是数组头元素下标减一,这里就是-1。
2、交换基准数和尾元素,因为现在我们是整个数组,所以就是数组的尾元素11。
在这里插入图片描述
3、进行数组的遍历,将数组中的元素和基准数进行比较,为了满足所有比基准数小的数都放到基准数前面,所有比基准数大的数都放到基准数后面,在每一轮的遍历中需要遵循着这样的规则:
**A、如果当前元素小于等于基准数时,首先分割指示器右移一位; **
B、在A的基础之上,如果当前元素下标大于分割指示器下标时,当前元素和分割指示器所指元素交换。
所以接下来随着数组的遍历,其中元素的变动情况是:
3.1、
在这里插入图片描述
3.2、
在这里插入图片描述
3.3、
在这里插入图片描述
移动了分区指示器,并交换了63和11
3.4、
在这里插入图片描述
移动了分区指示器,并交换了63和9
3.5、
在这里插入图片描述
3.6、
在这里插入图片描述
移动了分区指示器,并交换了63和24
3.7、
在这里插入图片描述
3.8、
在这里插入图片描述
移动了分区指示器,并交换了86和48,并完成了本次分区操作,、所有比基准数48小的数都已到它左边,所有比基准数48大的数都已到它右边。
3.9、48将数组分为左分区和右分区,左分区和右分区再按上面快速排序的方法分别继续排序下去,就可完成最终的排序。
所以总结一下,一次快速排序的过程包括有:

  • 1、选定基准数;
  • 2、交换基准数和尾元素;
  • 3、根据AB规则遍历元素;
  • 4、将以基准数为轴拆分出来的左分区和右分区,分别使用1~3点所说的快速排序过程,继续排序,这其实就是个递归过程。

希尔排序

一种基于插入排序的快速的排序算法(请大家先学习插入排序,了解基本的插入排序的思想。对于大规模乱序数组插入排序很慢,因为元素只能一点一点地从数组的一端移动到另一端。例如,如果主键最小的元素正好在数组的尽头,要将它挪到正确的位置就需要N-1 次移动。
希尔排序为了加快速度简单地改进了插入排序,也称为缩小增量排序,同时该算法是冲破O(n^2)的第一批算法之一。
希尔排序是把待排序数组按一定增量的分组,对每组使用直接插入排序算法排序;然后缩小增量继续分组排序,随着增量逐渐减少,每组包含的元素越来越多,当增量减至 1 时,整个数组恰被分成一组,排序便完成了。这个不断缩小的增量,就构成了一个增量序列。

	public static int [] shellArray(int [] nums){
        int current ;
        //按量分组后,每个分组中,temp代表当前待排序数组,该元素之前的组内元素均已被排序过
        //gap 是用来分组的增量,会依次递减
        int gap =nums.length/2;
        while (gap > 0) {
            for (int i = gap; i < nums.length; i++) {
                current=nums[i];
                //组内已被排序的数据索引
                int preIndex = i - gap;
                //组内已被排序过数据中倒序寻找合适的位置,
                // 如果当前待排序数据比 比较的元素小,则将比较的元素在组内后移一位
                while (preIndex >= 0 && nums[preIndex] > current) {
                    nums[preIndex+gap]=nums[preIndex];
                    preIndex-=gap;
                }
                //while循环结束时,说明已经找到了当前待排序数据的合适位置,插入
                nums[preIndex+gap]=current;
            }
            gap/=2;
        }
        return nums;
    }
希尔排序(降序)示例

选择增量的计算方式为:gap=数组的长度/2,缩小增量继续以gap = gap/2的方式,于是形成的增量序列为{7,3,1}。
在这里插入图片描述
1、第一个增量为7,则原始数组被分为7组,比如{35,72}为一组,{63,1}为一组,组内的每个元素之间数组下标之差为7,这7组分别进行插入排序
在这里插入图片描述
组内插入排序之后:
在这里插入图片描述
2、第二个增量为3,则第一次排序后数组被分为3组,比如{72,43,53,48,18}为一组,组内的每个元素之间数组下标之差为3,这3组分别进行插入排序
在这里插入图片描述
组内插入排序之后:
在这里插入图片描述
3、第三个增量为1,则第二次排序后数组被分为1组,进行插入排序,形成最后的排序数组
在这里插入图片描述

希尔排序中的增量序列

从理论上说,只要一个数组是递减的,并且最后一个值是1,都可以作为增量序列使用。有没有一个步长序列,使得排序过程中所需的比较和移动次数相对较少,并且无论待排序列记录数有多少,算法的时间复杂度都能渐近最佳呢?但是目前从数学上来说,无法证明某个序列是“最好的”。

常用的增量序列有
希尔增量序列 :{N/2, (N / 2)/2, …, 1},其中N为原始数组的长度,这是最常用的序列,但却不是最好的
Hibbard序列:{2^k-1, …, 3,1}
Sedgewick序列:{… , 109 , 41 , 19 , 5,1} 表达式为94i-92i+1 或者 4i-3*2i+1

归并排序

归并(Merge)排序法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。

public static int [] mergeArray(int [] nums){
        int mid = nums.length / 2;
        int[] left = Arrays.copyOfRange(nums, 0, mid);
        int[] right = Arrays.copyOfRange(nums, mid, nums.length);

        return merge(sortArray(left),sortArray(right));
    }

    private static int[] merge(int[] left, int[] right) {
        int[] result = new int[left.length + right.length];
        int i =0;
        int j =0;
        for (int index = 0; index < result.length; index++) {
            if (i>=left.length) {//左边数组已经取完,完全取右边数组的值即可
                result[index]=right[j++];
            }else if (j>= right.length){//右边数组已经取完,完全取左边数组的值即可
                result[index]=left[i++];
            }else if (left[i]> right[j]){//左边数组的值大于右边数组,去右边数组的值
                result[index]=right[j++];
            }else {
                result[index]=left[i++];//右边数组大于左边数组的值,取左边数组的值
            }
        }
        return result;
    }

堆排序

许多应用程序都需要处理有序的元素,但不一定要求他们全部有序,或者不一定要一次就将他们排序,很多时候,每次只需要操作数据中的最大元素(最小元素),那么有一种基于二叉堆的数据结构可以提供支持。
所谓二叉堆,是一个完全二叉树的结构,同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。在一个二叉堆中,根节点总是最大(或者最小)节点。
在这里插入图片描述
这就是一个典型的二叉堆。

堆排序算法就是抓住了这一特点,每次都取堆顶的元素,然后将剩余的元素重新调整为最大(最小)堆,依次类推,最终得到排序的序列。

public class HeapSort {
    //声明全局变量,用于记录数组array的长度;
    private static int len;

    /* 交换数组内两个元素*/
    public static void swap(int[] array, int i, int j) {
        int temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    public int[] sortArray(int[] nums) {
        len = nums.length;
        if (len < 1) return nums;
        /*1.构建一个最大堆*/
        buildMaxHeap(nums);
        /*2.循环将堆首位(最大值)与未排序数据末位交换,然后重新调整为最大堆*/
        while (len > 0) {
            swap(nums, 0, len - 1);
            len--;
            adjustHeap(nums, 0);
            System.out.println("--------------------");
        }
        return nums;
    }

    /**
     * 建立最大堆
     */
    public static void buildMaxHeap(int[] array) {
        /*从最后一个非叶子节点开始向上构造最大堆*/
        for (int i = (len/2-1); i >= 0; i--) {
            adjustHeap(array, i);
        }
        System.out.println("构造完成最大堆");
        System.out.println("============================================");
    }

    /**
     * 调整使之成为最大堆
     */
    public static void adjustHeap(int[] array, int i) {
        int maxIndex = i;
        int left = 2*i+1;
        int right = 2*(i+1);
        //左子树存在且左子树大于父节点,则将最大指针指向左子树
        if (left < len && array[left] > array[maxIndex])
            maxIndex = left;
        //右子树存在且右子树大于父节点和左节点,则将最大指针指向右子树
        if (right < len && array[right] > array[maxIndex]&&array[right]>array[left])
            maxIndex = right;
        /*如果父节点不是最大值,则将父节点与最大值交换,并且递归调整与父节点交换的位置。*/
        if (maxIndex != i) {
            swap(array, maxIndex, i);
            adjustHeap(array, maxIndex);
        }
    }
}

补充知识:完全二叉树

在这里插入图片描述
二叉树:是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”(right subtree)
满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点二叉树。
完全二叉树:是由满二叉树而引出来的,如果将一棵满二叉树由上到下,由左至右,每个结点都用数字编号,另外一个二叉树也同样由上到下,由左至右,每个结点都用数字编号,二叉树中的每个结点都可以在满二叉树中一一对应,称这个二叉树为完全二叉树。所以一棵满二叉树一定是个完全二叉树,而完全二叉树不是满二叉树。

完全二叉树的数组表示法

在这里插入图片描述
A:0 B:1 C:2 B=20+1 C=2(0+1)
B:1 D:3 E:4 D=21+1 E = 2(1+1)
由此可以退出两个重要的推论:
推论1:对于位置为K的结点 左子结点=2k+1 右子结点=2(k+1)
验证:C:2 22+1=5 2(k+1)=6
推论2:最后一个非叶节点的位置为 (N/2)-1,N为数组长度。

堆排序(降序)示例

在这里插入图片描述
将该数组视为一个完全二叉树 ,则是:
在这里插入图片描述

堆的初始化

很明显,这个二叉树不符合最大堆的定义,需要初始化为最大堆,从最后一个非叶节点开始,从下到上,从右到左调整
最后一个非叶节点在8/2-1=3的位置,也就是值为9的元素,将它和自己的叶节点进行比较并交换
在这里插入图片描述
48和63调整到位后,进而调整根节点35,
在这里插入图片描述
将35和它的子结点86交换,此时86变成根结点,35则变成子结点。很明显35和11、63组成的树不符合二叉堆的定义,此时需要再次调整35的位置:
在这里插入图片描述
此时就完成了堆的初始化,最大的数已经成为了根节点 。

正式开始堆排序的过程

此时将堆顶的86和尾元素9交换

在这里插入图片描述
86现在处于数组下标为7的位置,不再将86视为二叉树的一部分。9处于根结点,很明显,此时需要调整元素的位置 使之重新变成二叉堆
在这里插入图片描述
继续将堆顶63和尾元素48交换,63现在处于数组下标为6的位置,不再将63视为二叉树的一部分。48处于根结点,很明显,此时需要调整元素的位置 使之重新变成二叉堆
在这里插入图片描述
经过反复将堆顶元素和尾元素交换,并调整二叉堆的过程,最后数据变为

在这里插入图片描述

如果需要进行降序,改用最小堆即可。

桶排序

桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,利用某种函数的映射关系将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序)。

基本步骤是:

  • 根据输入建立适当个数的桶,每个桶可以存放某个范围内的元素;
  • 将落在特定范围内的所有元素放入对应的桶中;
  • 对每个非空的桶中元素进行排序,可以选择通用的排序方法,比如插入、快排;
  • 按照划分的范围顺序,将桶中的元素依次取出。排序完成。

桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。

桶排序(降序)示例

可以建立5个桶,每个桶按范围顺序依次是[0, 10)、[10, 20)…[40, 50),注意是左闭右开区间,对于待排序数组,5,9会被放到[0, 10)这个桶中,…,48会被放到[40, 50)这个桶中
在这里插入图片描述

在这里插入图片描述
对这5个桶中的元素分别排序。 依次取出5个桶中元素,得到排序后的序列。
在桶排序中保证元素均匀分布到各个桶尤为关键。举个反例,有数组[0, 9, 4, 5, 8, 7, 6, 3, 2, 1]要排序,它们都是10以下的数,如果还按照上面的范围[0, 10)建立桶,全部的元素将进入同一个桶中,此时桶排序就失去了意义。实际情况很可能事先就不知道输入数据是什么,为了保证元素均匀分不到各个桶中,需要建立多少个桶,每个桶的范围是多少呢?
其实可以这样:
其实可以这样:简单点,首先限定桶的容量,再根据元素的个数来决定桶的个数。当然使用更复杂的方法也是可以的。
桶排序利用函数的映射关系,减少了几乎所有的比较工作。实际上,桶排序的f(k)值的计算,其作用就相当于快排中划分,已经把大量数据分割成了基本有序的数据块(桶)。然后只需要对桶中的少量数据做先进的比较排序即可。

public List<Integer> bucketArray(List<Integer>nums,int bucketCap){
        if (nums == null||nums.size()<2) {
            return nums;
        }
        int max =nums.get(0);
        int min =nums.get(0);
        for (int i = 0; i < nums.size(); i++) {
            if (nums.get(i)>max){
                max= nums.get(i);
            }
            if (nums.get(i)<min) {
                min=nums.get(i);
            }
        }
        //桶的数量
        int bucketCount = (max - min) / bucketCap + 1;
        //构建桶
        ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketCount);
        ArrayList<Integer> resultArr = new ArrayList<>(bucketCount);
        for (int i = 0; i < bucketCount; i++) {
            bucketArr.add(new ArrayList<Integer>());
        }

        //将数组的数据分配到桶中
        for (int i = 0; i < nums.size(); i++) {
            bucketArr.get(nums.get(i)-min/bucketCap).add(nums.get(i));
        }
        for (int i = 0; i < bucketCount; i++) {
            if (bucketCap==1) {
                for (int j = 0; j < bucketArr.get(i).size(); j++) {
                    resultArr.add(bucketArr.get(i).get(j));
                }
            }else {
                if (bucketCount==1) {
                    bucketCount--;
                    //对桶中的数据再次用桶进行排序
                    List<Integer> temp = bucketArray(bucketArr.get(i), bucketCap);
                    for (int j = 0; j < temp.size(); j++) {
                        resultArr.add(temp.get(j));
                    }
                }
            }
        }
        return resultArr;
    }

排序算法总结

(LeetCode- 912) 排序数组
对于排序,LeetCode上有个912号排序数组的题目,可以自行实现了代码后测试下性能。
在这里插入图片描述
计数排序中的K为整数的范围;
基数排序时间复杂度为O(NM),其中N为数据个数,M为数据位数;
桶排序的时间复杂度为O(n+c),但是c比较复杂,c=n
(logn-logk),其中n表示待排数据的个数,k表示桶的个数。

名词解释

算法的稳定性

稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面;
不稳定:如果a原本在b的前面,而a=b,排序之后a可能会出现在b的后面;
排序算法如果是稳定的,那么从一个键上排序,然后再从另一个键上排序,前一个键排序的结果可以为后一个键排序所用。

排序算法时间复杂度助记

冒泡、选择、插入排序需要两个for循环,每次只关注一个元素,平均时间复杂度为O()(外循环找元素O(n),内循环找位置O(n))
快速、归并、希尔、堆基于分治思想,log以2为底,平均时间复杂度往往和O(nlogn)(外循环找元素O(n),内循环找位置O(logn))相关

快速排序的优势

从平均时间来看,快速排序是效率最高的:
快速排序中平均时间复杂度O(nlog n),这个公式中隐含的常数因子很小,比归并排序的O(nlog n)中的要小很多,所以大多数情况下,快速排序总是优于归并排序的。
而堆排序的平均时间复杂度也是O(nlog n),但是堆排序存在着重建堆的过程,它把根节点移除后,把最后的叶子结点拿上来,是为了重建堆,但是,拿上的值是要比它的两个叶子结点要差很多的,它要比较很多次,才能回到合适的位置。堆排序就会有很多的时间耗在堆调整上。
虽然快速排序的最坏情况为排序规模(n)的平方关系,但是这种最坏情况取决于每次选择的基准, 对于这种情况,已经提出了很多优化的方法,比如三取样划分和Dual-Pivot快排。
同时,当排序规模较小时,划分的平衡性容易被打破,而且频繁的方法调用超过了O(nlog n)为O()省出的时间,所以一般排序规模较小时,会改用插入排序或者其他排序算法。

查找算法

(LeetCode-704) 二分查找
题目
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

分析和解答

查找算法概论

查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算。常见的查找算法有七种:

  1. 顺序查找
  2. 二分查找
  3. 插值查找
  4. 斐波那契查找
  5. 树表查找
  6. 分块查找
  7. 哈希查找

顺序查找
又称为线性查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
二分查找
也成为折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。折半查找的前提条件是需要有序表顺序存储。
比如下面的数组寻找100的过程
在这里插入图片描述
二分查找的时间复杂度:O(logn)。
插值查找
为什么上述算法一定要是折半,而不是折四分之一或者折更多呢?
打个比方,在英文字典里面查“apple”,你下意识翻开字典是翻前面的书页还是后面的书页呢?如果再让你查“zoo”,你又怎么查?很显然,这里你绝对不会是从中间开始查起,而是有一定目的的往前或往后翻。
同样的,比如要在取值范围1 ~ 10000 之间 100 个元素从小到大均匀分布的数组中查找5, 自然会考虑从数组下标较小的开始查找。
经过以上分析,折半查找这种查找方式,不是自适应的(也就是说是傻瓜式的)。二分查找中查找点计算如下:
mid=(low+high)/2;
通过类比,可以将查找的点改进为如下:
mid=low+(key-a[low])/(a[high]-a[low])*(high-low),
也就是将上述的比例参数1/2改进为自适应的,根据关键字在整个有序表中所处的位置,让mid值的变化更靠近关键字key,这样也就间接地减少了比较次数。
基本思想:基于二分查找算法,将查找点的选择改进为自适应选择,可以提高查找效率。当然,插值查找也属于有序查找。
注:对于表长较大,而关键字分布又比较均匀的查找表来说,插值查找算法的平均性能比折半查找要好的多。反之,数组中如果分布非常不均匀,那么插值查找未必是很合适的选择。

斐波那契查找
在介绍斐波那契查找算法之前,先介绍一下很它紧密相连并且大家都熟知的一个概念——黄金分割。
黄金比例又称黄金分割,是指事物各部分间一定的数学比例关系,即将整体一分为二,较大部分与较小部分之比等于整体与较大部分之比,其比值约为1:0.618或1.618:1。
0.618被公认为最具有审美意义的比例数字,这个数值的作用不仅仅体现在诸如绘画、雕塑、音乐、建筑等艺术领域,而且在管理、工程设计等方面也有着不可忽视的作用。因此被称为黄金分割。
大家记不记得斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…….(从第三个数开始,后边每一个数都是前两个数的和)。然后会发现,随着斐波那契数列的递增,前后两个数的比值会越来越接近0.618,利用这个特性,就可以将黄金比例运用到查找技术中。
基本思想:也是二分查找的一种提升算法,通过运用黄金比例的概念在数列中选择查找点进行查找,提高查找效率。同样地,斐波那契查找也属于一种有序查找算法。

树表查找
最简单的树表查找算法就是二叉树查找算法。
二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围。 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树。
一般来说,一个有序的数组是很容易转化为二叉查找树的。但是对二叉查找树的维护要付出额外的花费,因为数据变化后,要依然能够满足二叉查找树,这一点上和排序中的二叉堆一样。
在这里插入图片描述
在这里插入图片描述
而且插入和删除元素的时候,树如果没有保持平衡,很容易退化为顺序查找,所以在这个之上就产生了AVL树和红黑树,以保持树的平衡。
在这里插入图片描述
分块查找
又称为索引顺序查找,是顺序查找的一种改进方式.。先把线性表分成若干块,每块包含若干个元素;每个块内无序,块间有序[ 每个当前块中的最大值小于下一个块的任意值 ];建立一个索引表,把 每块中的最大关键字值和每块的第一个元素在表中的位置 和最后一个元素在表中的位置存放在索引项中;先确定待查数据元素所在的块,然后再块内顺序查找。
这就有点像数据库中的B+树。

哈希查找
这个就很简单了,就是用一个hash表来存放待查找的数据。这种方法是很多同学都已经非常熟悉的做法了,这里不再赘述。

Part2

位运算和应用

位运算知识

二进制

要了解位运算,首先要搞明白进制的概念。
从小学一年级开始,就开始学加减乘除,特别是学加法的时候,刚开始算超过十的时候,如果不熟练,会掰着手指头数,有时候恨不得把脚趾头也加上。正是因为一般人有十个手指头,所以,从老老。。。祖先开始就用十进制。逢十进一,意味着至少两点,1,每个位上的数字不能超过10,最大只能到9,第二,如果某个位上的数字因为运算超过了10,怎么办?往高位进位。
208=208 = 2100+010+81 = 2102+0101+8100

在这里插入图片描述
那计算机怎么办?计算机又没十个手指头。自然界要找出有十种状态的东西很难,但是只有两种状态的东西或者现象还是很容易的,所以计算机中要用二进制。
二进制就是逢二进一,不能说你二进制比较二,规则就不一样了,基本法还是要遵守的。所以二进制里同样要遵循:1,每个位上的数字不能超过2,最大只能到1,第二,如果某个位上的数字因为运算超过了2,怎么办?往高位进位。
(10进制)208 = 127+126+025+124+023+022+021+020
= 1128+164+032+116+08+04+02+01
= 11010000(2进制)
在这里插入图片描述

负数的表示

  计算机中负数的表示,是以补码的形式呈现的。
  原码:一个正数,按照绝对值大小转换成的二进制数;一个负数按照绝对值大小转换成的二进制数,然后最高位补1,称为原码。
  比如 00000000 00000000 00000000 00000101 是 5的 原码。
  10000000 00000000 00000000 00000101 是 -5的 原码。
  反码:正数的反码与原码相同,负数的反码为对该数的原码除符号位外各位取反。
  取反操作指:原为1,得0;原为0,得1。(1变0; 0变1)
  比如:正数00000000 00000000 00000000 00000101 的反码还是 00000000 00000000 00000000 00000101
  负数10000000 00000000 00000000 00000101每一位取反(除符号位),得11111111 11111111 11111111 11111010。
  称:11111111 11111111 11111111 11111010 是 10000000 00000000 00000000 00000101 的反码。
  补码:正数的补码与原码相同,负数的补码为对该数的原码除符号位外各位取反,然后在最后一位加1.
  比如:10000000 00000000 00000000 00000101 的反码是:11111111 11111111 11111111 11111010。
  那么,补码为:
  11111111 11111111 11111111 11111010 + 1 = 11111111 11111111 11111111 11111011
  所以,-5 在计算机中表达为:11111111 11111111 11111111 11111011。
  整数-1在计算机中如何表示。
  假设这也是一个int类型,那么:
  1、先取-1的原码:10000000 00000000 00000000 00000001
  2、得反码: 11111111 11111111 11111111 11111110(除符号位按位取反)
  3、得补码: 11111111 11111111 11111111 11111111
  可见,-1在计算机里用二进制表达就是全1。
  负数为何在计算机中要这么表示?人类的减法运算涉及到符号位的处理,这对电路来说,是比较复杂的。补码系统下,可以通过把减法换成反码,然后通过加法器运算,这样就可以避免设计复杂的减法电路。

常用位运算

  按位与 & (1&1=1 0&0=0 1&0=0)
  按位或 | (1|1=1 0|0=0 1|0=1)
  按位非 (1=0 ~0=1)
  按位异或 ^ (1^1=0 1^0=1 0^0=0,很明显任何一个数和自己异或结果一定是0)
  有符号右移>>(若正数,高位补0,负数,高位补1)
  有符号左移<<
  无符号右移>>>(不论正负,高位均补0)

具体位运算的运行结果参见代码cn.tulingxueyuan.bit.base. IntToBinary

位运算运用场景和优劣势

  JDK中大量使用了位运算,比如Class类中判断是否注解isAnnotation,网络通信相关的SelectionKey中判断网络事件等等。

  Java容器中的HashMap和ConcurrentHashMap的实现.
  权限控制或者商品属性,可以参见Permission.java
  简单可逆加密,比如异或运算(1^1=0 ; 0^1=1 )
  在程序中使用位运算能节省很多代码量、运行效率高、比较节约空间、最大的问题是不直观。

只出现一次的数字

在这里插入图片描述
题目为什么要强调有一个数字出现一次,其他的出现两次?
想到了异或运算的性质:任何一个数字异或它自己都等于0。
在这里插入图片描述

也就是说,如果从头到尾依次异或数组中的每一个数字,那么最终的结果刚好是那个只出现依次的数字,因为那些出现两次的数字全部在异或中抵消掉了。

	Map<Integer, Integer> map = new HashMap<>();
        int [] nums = {2,2,1};
        //存入map
        for (int num : nums) {
            map.put(num,0);
        }
        //遍历&准备容器
        ArrayList<Integer> list = new ArrayList<>();
        for (int num : nums) {
            Integer value = map.get(num);
            map.put(num,value+1);
        }
        for (Integer integer : map.keySet()) {
            if (map.get(integer)!=1) {
                list.add(integer);
            }
        }
	public int singleNumber(int[] nums){
        int result = 0;
        for (int num : nums) {
            result = result ^ num;  //任何数字与自己异或结果一定是0
        }
        return result;
    }

比特位计数

在这里插入图片描述

	public int[] countBits(int num){
        int[] bits = new int[num + 1];
        for (int i = 1; i <= num; i++) {
            bits[i]=bits[i&(i-1)]+1;
        }
        return bits;
    }

汉明距离

在这里插入图片描述

public int hanmingDistance(int x ,int y){
       int distance = 0;
       for (int i = x^y;i!=0;i&=(i-1)){
           distance++;
       }
       return distance;
   }

动态规划

学习动态规划

从游戏入门动态规划

在学习什么是动态规划前,先来玩一个游戏,
现在你在某个游戏中做任务,现在接到的任务是,从下面的三样物品中进行挑选放进背包,数量并不限,然后穿越迷宫,找到游戏中的特定NPC,把这些物品给他看,作为信物然后获得下个关卡的道具钥匙。
在这里插入图片描述
断玉刀,价值1500两,占据1个格子
在这里插入图片描述
天蚕甲,价值3000两,占据4个格子
在这里插入图片描述
金精玉魄剑,价值2000两,占据3个格子
看下背包,现在包里只剩下4个格子了,因为这些物品在获得道具钥匙后可以出售,所以希望放到背包里的物品的总价值自然就是越高越好,怎么选择呢?
最简单的办法如下:尝试各种可能的商品组合,并找出价值最高的组合。很明显,总有8种组合:
在这里插入图片描述
这样可行,但速度非常慢。在有3件商品的情况下,你需要计算8个不同的集合;有4件商品时,你需要计算16个集合。每增加一件商品,需要计算的集合数都将翻倍!这种算法的时间复杂度为O(2n),非常非常慢。
可以尝试着用动态规划的思想来解决这个问题,请记住,只是思想,而不是动态规划的定义,所以对动态规划完全不知道,也不会影响对下面的解题过程的理解。动态规划的思想是先解决子问题,再逐步解决大问题。
对于上面的背包问题,你先解决小背包(子背包)问题,再逐步解决原来的问题。

准备工作
首先画个表格
在这里插入图片描述
表格的各行表示可选的物品,各列为不同容量(1~4格)的背包,每个空白的单元格表示当前背包容量下选择物品后的最大价值。一行行看下来。

第一行
毫无疑问,因为第一行只有断玉匕可选,所以不管背包有多大,总价值只有1500两。
在这里插入图片描述
第二行
可选的物品有断玉匕和金精玉魄剑。天蚕甲目前还不能选。
先来看第一个单元格,它表示容量为1格的背包。在此之前,可装入1磅背包的商品的最大价值为1500两。
该不该选金精玉魄剑呢?背包的容量为1格,能装下金精玉魄剑吗?当然不行,金精玉魄剑需要三个格子,因此最大价值依然是1500两。
接下来的单元格的情况与此相同。背包的容量分别为2格,而以前的最大价值为1500两。由于这些背包装不下金精玉魄剑,因此最大价值保持不变。
在这里插入图片描述
背包容量为3格呢?终于能够装下金精玉魄剑了!拿断玉匕,则原来的最大价值为1500两,但如果在背包中装入金精玉魄剑而不是断玉匕,价值将为2000两!因此还是拿金精玉魄剑。
在这里插入图片描述
背包容量为4格呢?,毫无疑问按照在背包只有3格的处理,拿金精玉魄剑比拿断玉匕更有价值,但是现在还剩了1格,怎么办?可以看见在1格的时候能填充的最大值时是放入断玉匕。很自然的,在背包中同时装入两者,比原来4格时的最大价值为1500两变为了价值将为3500两!
在这里插入图片描述
更新了最大价值!如果背包的容量为4格,就能装入价值至少3500两的物品。
第三行
三者都可选。毫无疑问1个和2格都只能选断玉匕,价值1500两,3格放入金精玉魄剑更有价值。
在这里插入图片描述
4格呢?现在可以放入天蚕甲,价值3000两,但是4格时现在能放入的最大值为3500两,所以应该放入断玉匕+金精玉魄剑。
在这里插入图片描述
这样就获得最后的结果,4格背包的情况下,拿断玉匕+金精玉魄剑是具有最大价值的。

小结
其实,在我们前面的过程中,在每个单元格的处理中其实遵循了一个算法规则或者说公式:
在这里插入图片描述
这也是我们为何计算小背包可装入的物品的最大价值,当有剩余空间时,可以根据这些子问题的答案来确定余下的空间可装入哪些物品及其价值。
行的排列顺序发生变化时对我们的最终结果没有影响。感兴趣的同学可以自己试试。

从旅游安排继续了解动态规划

外出旅游到北京,准备在北京旅游3天,想去游览的地方很多,没法前往每个地方游览,因此列个单子。
在这里插入图片描述
对于想去游览的每个名胜,都列出所需的时间以及你有多想去看看。根据这个清单,我们希望在有限的时间游览足够的景点使自己内心的评分总值最高。怎么做呢?和我们上面游戏的例子其实是一样的。这个例子我们就不说明的非常详细了。
过程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
虽然上图标出了从(故宫,2.5天)以及(颐和园,2.5天)都可以组成(八达岭长城,3天)的结果值,不过为了统一化处理,我们规定本行的结果值来自上一行,在上图中也就是(八达岭长城,3天)的结果来自它的上一行,也就是(颐和园,2.5天)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最终的结果及其路径:
在这里插入图片描述
可以看见,去游览天安门广场、颐和园、八达岭长城、天坛、恭王府是我们的最佳答案,刚好耗时3天,总得分36分。
要注意,最优解是完全可能导致时间安排不满或者背包没装满的情况产生的,比如旅游计划里,还想去后海的酒吧来一场艳遇,内心评分50分,远远超过其他景点。但是需要的时间因为白天需要休息和打扮,晚上去通宵,所以估计的时间为2.8天。这种情况下,只能去后海的酒吧,余下的时间只有0.2天了,哪里也去不了了。

以旅游安排的例子来看一下,表格的的代号R。
在这里插入图片描述
最终的结果36=R(7,6) = 恭王府+R(6,5)<>R(6,6),说明7-恭王府在最终结果中,
R(6,5) = R(5,5),说明6-圆明园不在最终结果中
R(5,5) = 天坛+R(4,4)<>R(4,5),说明5-天坛在最终结果中
R(4,4) = 八达岭长城+R(3,3)<>R(3,4),说明4-八达岭长城在最终结果中
R(3,3) = 颐和园+R(2,1)<>R(2,3),说明3-颐和园在最终结果中
R(2,1) = R(1,1),说明2-故宫不在最终结果中
R(1,1) = 天安门广场,说明1-天安门广场在最终结果中
通过这个例子我们可以看到,由最终的结果是能够反推出哪些物品组成了最终的结果,那就说明类ArrayElement是可以不要的,或者说可以简化的。
同时为了避免要对物品数组中的第一个实际物品做额外处理,且存在着一定的bug隐患,所以我们对二维数组还可以额外的加入一行一列,值都为0,或者理解为物品数组中存在着一个价值为0,耗费也为0的虚拟物品,并且放在物品数组的第0号下标位置。
小结
从上面两个例子我们可以看到,我们把问题分解为多个阶段,每个阶段对应一个决策。我们记录每一个阶段可达的结果集合,然后通过当前阶段的结果集合,来推导下一个阶段的结果集合,动态地往前推进。这也是动态规划这个名字的由来。

动态规划的定义

其实,上面所说的不管是游戏物品的选择,还是旅游的安排都是动态规划中的经典题目–背包问题,所以到底什么是动态规划?
到现在,可以对动态规划下个学术定义了。

动态规划(Dynamic Programming,简称DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域。
在现实生活中,有一类活动,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。因此各个阶段决策的选取不能任意确定,它依赖于当前面临的状态,又影响以后的发展。
所以如果一类活动过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策,每一个阶段都有若干个策略可供选择,一个阶段的策略确定以后,形成了本阶段的决策,常常影响到下一个阶段的决策,从而就完全确定了一个过程的活动路线,则称它为多阶段决策问题
当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线。在多阶段决策问题中,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化的过程为动态规划方法
很明显,既然每一个阶段都有若干个策略可供选择,每个策略都可以带来不同的效果,多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果,一般来说,而每一个阶段的决策在当前阶段看来就是最优的决策。
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,分治法基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解,但是分治法里子问题之间往往是互相独立的,可以并行求解。
与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。有些子问题的求解会影响后面其他子问题的求解,而且有些子问题会被重复计算很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。
我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
当然,动态规划中还有无后效性、最优化原理等等概念,但是这个对我们目前做题没什么影响,所以我们暂时不去了解。

动态规划的解题步骤

对于一个动态规划题目,有没有什么比较通用的求解步骤呢?可以采用下面的办法:
1、确定状态转移公式,当前的状态是怎么由前面的状态变化而来的及其与之相关联的辅助的dp数组(dp table)以及下标的含义。这一步往往也是最难的,这一步想清楚了,整个动态规划的问题基本上可以说就解决了一大半。一般来说,首先要确定dp数组中元素代表的意义,然后在这个意义之下,确定状态是如何在dp数组的元素之间如何变化的。
2、初始化dp数组。
3、根据题目条件开始遍历,并实现状态转移公式。
同时在实现的过程中,可以适当的输出dp数组的值,确定自己的代码实现思路无误。具体怎么做,我们用一个实际的题目来说明。


JAVA 算法

二分查找

又叫折半查找,要求待查找的序列有序。每次取中间位置的值与待查关键字比较,如果中间位置的值比待查关键字大,则在前半部分循环这个查找的过程,如果中间位置的值比待查关键字小,则在后半部分循环这个查找的过程。直到查找到了为止,否则序列中没有待查的关键字。

    public static int biSearch(int []array,int a){    
	  int lo=0;    
	  int hi=array.length-1;    
	  int mid;       
	  while(lo<=hi){         
		  mid=(lo+hi)/2;//中间位置       
		  if(array[mid]==a){           
		  	return mid+1; 
	      }else if(array[mid]<a){ //向右查找         
	        lo=mid+1;         
	      }else{ //向左查找          
	            hi=mid-1; 
	      } 
	  }     
	  return -1; 
} 


插入排序算法

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应的位置并插入。插入排序非常类似于整扑克牌。在开始摸牌时,左手是空的,牌面朝下放在桌上。接着,一次从桌上摸起一张牌,并将它插入到左手一把牌中的正确位置上。为了找到这张牌的正确位置,要将它与手中已有的牌从右到左地进行比较。无论什么时候,左手中的牌都是排好序的。

如果输入数组已经是排好序的话,插入排序出现最佳情况,其运行时间是输入规模的一个线性函数。如果输入数组是逆序排列的,将出现最坏情况。平均情况与最坏情况一样,其时间代价是(n2)。

 public static void insertionSort(int arr[]){ 
        for (int i = 1; i < arr.length; i++) {
           //插入的数            
           int key = arr[i];
           //被插入的位置(准备和前一个数比较) 
           int j = i - 1;
          //如果插入的数比被插入的数小 
          while (j >= 0 && arr[j] > key) {
                  //将把 arr[index] 向后移动    将较大的元素向后移动一位                         
                  arr[j + 1] = arr[j];
                   //让 index 向前移动   更新 index,继续向前比较
                  j--;
               } 
                //把插入的数放入合适位置 
             arr[j + 1] = key;
        } 
  } 

	public static void main(String[] args) {
        int[] arr = {12, 11, 13, 5, 6};
        insertionSort(arr);
        System.out.println("排序后的数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }

快速排序算法

快速排序的原理:选择一个关键值作为基准值。比基准值小的都在左边序列(一般是无序的),比基准值大的都在右边(一般是无序的)。一般选择序列的第一个元素。

一次循环:从后往前比较,用基准值和最后一个值比较,如果比基准值小的交换位置,如果没有继续比较下一个,直到找到第一个比基准值小的值才交换。找到这个值之后,又从前往后开始比较,如果有比基准值大的,交换位置,如果没有继续比较下一个,直到找到第一个比基准值大的值才交换。直到从前往后的比较索引>从后往前比较的索引,结束第一次循环,此时,对于基准值来说,左右两边就是有序的了。

   public class QuickSort {
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 划分数组,获取分界点 pivot
            int pivot = partition(arr, low, high);
            // 对左侧子数组进行快速排序
            quickSort(arr, low, pivot - 1);
            // 对右侧子数组进行快速排序
            quickSort(arr, pivot + 1, high);
        }
    }

    public static int partition(int[] arr, int low, int high) {
        // 选取数组最后一个元素作为基准值
        int pivot = arr[high];
        // 初始化左侧子数组的结束索引
        int i = low - 1;

        // 遍历数组,将小于基准值的元素交换到左侧
        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++;
                swap(arr, i, j);
            }
        }

        // 将基准值交换到正确的位置
        swap(arr, i + 1, high);

        // 返回基准值的索引
        return i + 1;
    }

    public static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr = {5, 3, 7, 2, 8, 6, 1, 4};
        quickSort(arr, 0, arr.length - 1);
        System.out.println("排序后的数组:");
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

希尔排序算法

基本思想:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。

  1. 操作方法:

选择一个增量序列 t1,t2,…,tk,其中 ti>tj,tk=1;

  1. 按增量序列个数 k,对序列进行 k 趟排序;

  2. 每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

 private void shellSort(int[] a) { 
 	 	 int dk = a.length/2;   	
         while( dk >= 1  ){   
 	               ShellInsertSort(a, dk);    	     
                   dk = dk/2; 
 	 	 } 
 	} 
 	private void ShellInsertSort(int[] a, int dk) { 
//类似插入排序,只是插入排序增量是 1,这里增量是 dk,把 1 换成 dk 就可以了 
 	 	for(int i=dk;i<a.length;i++){ 
 	 	 	if(a[i]<a[i-dk]){ 
 	 	 	 	int j; 
 	 	 	 	int x=a[i];//x 为待插入元素  	 	 
                a[i]=a[i-dk]; 
 	 	 	 	for(j=i-dk;  j>=0 && x<a[j];j=j-dk){ 
//通过循环,逐个后移一位找到要插入的位置。 
 	 	 	 	 	a[j+dk]=a[j]; 
 	 	 	 	} 
 	 	 	 	a[j+dk]=x;//插入 
 	 	 	} 
 	 	} 
 	} 

桶排序算法

桶排序的基本思想是: 把数组 arr 划分为 n 个大小相同子区间(桶),每个子区间各自排序,最后合并 。计数排序是桶排序的一种特殊情况,可以把计数排序当成每个桶里只有一个元素的情况。

1.找出待排序数组中的最大值 max、最小值 min

2.我们使用 动态数组 ArrayList 作为桶,桶里放的元素也用 ArrayList 存储。桶的数量为(maxmin)/arr.length+1

3.遍历数组 arr,计算每个元素 arr[i] 放的桶

4.每个桶各自排序

  public static void bucketSort(int[] arr){ 
    
  int max = Integer.MIN_VALUE;  
  int min = Integer.MAX_VALUE; 
  for(int i = 0; i < arr.length; i++){ 
 max = Math.max(max, arr[i]);    
 min = Math.min(min, arr[i]); 
  } 
   //创建桶 
  int bucketNum = (max - min) / arr.length + 1; 
  ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);   for(int i = 0; i < bucketNum; i++){   
  bucketArr.add(new ArrayList<Integer>()); 
  } 
     //将每个元素放入桶 
  for(int i = 0; i < arr.length; i++){   
  int num = (arr[i] - min) / (arr.length);     bucketArr.get(num).add(arr[i]); 
  } 
     //对每个桶进行排序 
  for(int i = 0; i < bucketArr.size(); i++){ 
    Collections.sort(bucketArr.get(i)); 
  } 
 } 
 

基数排序算法

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。然后,从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成以后,数列就变成一个有序序列。

 public class radixSort {     
 inta[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,101,56,17,18,23,34,15,35,2
5,53,51};       
public radixSort(){      
sort(a);   
       for(inti=0;i<a.length;i++){                 System.out.println(a[i]);   
       }   
    }          
    public  void sort(int[] array){     
       //首先确定排序的趟数;     
       int max=array[0];         
       for(inti=1;i<array.length;i++){           
       if(array[i]>max){     
             max=array[i];     
            }     
       }     
       int time=0;            //判断位数;            while(max>0){               max/=10;                time++;     
       }    
        //建立 10 个队列;     
       List<ArrayList> queue=newArrayList<ArrayList>();            for(int i=0;i<10;i++){     
              ArrayList<Integer>queue1=new ArrayList<Integer>();              queue.add(queue1);     
       }     
          //进行 time 次分配和收集;     
       for(int i=0;i<time;i++){                //分配数组元素;               for(intj=0;j<array.length;j++){     
               //得到数字的第 time+1 位数;   
                 int x=array[j]%(int)Math.pow(10,i+1)/(int)Math.pow(10, i);                    ArrayList<Integer>queue2=queue.get(x);                    queue2.add(array[j]);                    queue.set(x, queue2);   
          }    
          int count=0;//元素计数器;     
          //收集队列元素;               for(int k=0;k<10;k++){                  while(queue.get(k).size()>0){   
                   ArrayList<Integer>queue3=queue.get(k);                      array[count]=queue3.get(0);                        queue3.remove(0);                      count++;   
               }    
          }     
       }                
    }   
} 

剪枝算法

在搜索算法中优化中,剪枝,就是通过某种判断,避免一些不必要的遍历过程,形象的说,就是剪去了搜索树中的某些“枝条”,故称剪枝。应用剪枝优化的核心问题是设计剪枝判断方法,即确定哪些枝条应当舍弃,哪些枝条应当保留的方法。

回溯算法

回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。

最短路径算法

从某顶点出发,沿图的边到达另一顶点所经过的路径中,各边上权值之和最小的一条路径叫做最短路径。解决最短路的问题有以下算法,Dijkstra 算法,Bellman-Ford 算法,Floyd 算法和 SPFA 算法等。

最大子数组算法

最长公共子序算法

最小生成树算法

现在假设有一个很实际的问题:我们要在 n 个城市中建立一个通信网络,则连通这 n 个城市需要布置 n-1 一条通信线路,这个时候我们需要考虑如何在成本最低的情况下建立这个通信网?

于是我们就可以引入连通图来解决我们遇到的问题,n 个城市就是图上的 n 个顶点,然后,边表示两个城市的通信线路,每条边上的权重就是我们搭建这条线路所需要的成本,所以现在我们有n个顶点的连通网可以建立不同的生成树,每一颗生成树都可以作为一个通信网,当我们构造这个连通网所花的成本最小时,搭建该连通网的生成树,就称为最小生成树。构造最小生成树有很多算法,但是他们都是利用了最小生成树的同一种性质:MST 性质(假设

N=(V,{E})是一个连通网,U 是顶点集 V 的一个非空子集,如果(u,v)是一条具有最小权值的边,其中 u 属于U,v 属于V-U,则必定存在一颗包含边(u,v)的最小生成树),下面就介绍两种使用 MST 性质生成最小生成树的算法:普里姆算法和克鲁斯卡尔算法。

交替打印1-100

public class PrintOddEven {
    private static final Object lock = new Object();
    private static int count = 1;
    private static final int MAX_COUNT = 100;

    public static void main(String[] args) {
        Thread oddThread = new Thread(new OddPrinter(), "OddThread");
        Thread evenThread = new Thread(new EvenPrinter(), "EvenThread");

        oddThread.start();
        evenThread.start();
    }

    static class OddPrinter implements Runnable {
        @Override
        public void run() {
            while (count <= MAX_COUNT) {
                synchronized (lock) {
                    if (count % 2 == 1) {
                        System.out.println(Thread.currentThread().getName() + ": " + count++);
                        lock.notify(); // 唤醒等待的线程
                    } else {
                        try {
                            lock.wait(); // 当前线程等待
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

    static class EvenPrinter implements Runnable {
        @Override
        public void run() {
            while (count <= MAX_COUNT) {
                synchronized (lock) {
                    if (count % 2 == 0) {
                        System.out.println(Thread.currentThread().getName() + ": " + count++);
                        lock.notify(); // 唤醒等待的线程
                    } else {
                        try {
                            lock.wait(); // 当前线程等待
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }
}
  • 29
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值