Leetcode(力扣)超高频题讲解(二)

高频题(二)

目录:
Leetcode(力扣)超高频题讲解(一)

一、相交链表(160)

题目描述:

编写一个程序,找到两个单链表相交的起始节点,未相遇返回null,如下图所示:

image-20210527103401481

1. 双指针

思路:

image-20210527103621995

A走a + c + b(注意字母顺序),B走b + c + a(注意字母顺序)时二者可以在c1相遇,也就是说当A走完a + c之后应该将B的头节点赋值给A,让A走b距离,当B走完b + c之后应该将A的头节点赋值给B,让B走a距离,二者即可相遇。

假如A与B没有相交节点,即两个链表没有相同的那部分c,A走的距离是a+b,B走的距离是a+b,二者走的距离相同,最后的时刻一定是同时到达相交节点(值为null),直接返回null即可。

代码实现:

public class Solution {
    
    

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    
    
        //特殊值的判断
        if (headA == null || headB == null) {
    
    
            return null;
        }

        //两个指针,分别用来遍历两个链表
        //由于需要将头节点赋值给另一个链表,所以头节点要保留
        ListNode head1 = headA;
        ListNode head2 = headB;

        while (head1 != head2) {
    
     //每次循环一人走一步
            
            //head1要么后移,要么移动到B链表的头部
            if (head1 != null) {
    
    
                head1 = head1.next;
            } else {
    
    
                head1 = headB;
            }

            //head2要么后移,要么移动到A链表的头部
            if (head2 != null) {
    
    
                head2 = head2.next;
            } else {
    
    
                head2 = headA;
            }
        }
        return head1;
    }
}

时间复杂度:O(m+n)

空间复杂度:O(1)

2. 哈希表

思路:

遍历链表A,将链表中的所有节点添加到哈希表中,然后遍历链表B,检查链表B中的每一个节点有没有存在于哈希表中的,如果存在,则为相交节点,如果不存在,返回null。

代码实现:

public class Solution {
    
    

    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    
    

        //特殊值的判断
        if (headA == null || headB == null) {
    
    
            return null;
        }

        //哈希表
        Set<ListNode> hashSet = new HashSet<>();

        //遍历链表A,添加所有节点到哈希表
        ListNode curNode = headA;
        while (curNode != null) {
    
    
            hashSet.add(curNode);
            curNode = curNode.next;
        }

        //遍历链表B,判断节点是否存在于哈希表中
        curNode = headB;
        while (curNode != null) {
    
    
            if(hashSet.contains(curNode)){
    
    
                return curNode;
            }
            curNode = curNode.next;
        }
        return null;
    }
}

时间复杂度:O(m+n)

空间复杂度:O(m) 或 O(n)

二、二叉树的先序、中序、后序遍历(144、94、145)

关于前序、后序的思路在初级班知识点中已总结,这里只放代码,仅讲解中序遍历。

1. 前序

递归:

class Solution {
    
    
    public List<Integer> preorderTraversal(TreeNode root) {
    
    
        List<Integer> arrayList = new ArrayList<>();
        if(root == null) {
    
    
            return arrayList; 
            //注意查看网页的题目要求,root为null时不能返回null值,要返回空List
        }
        pre(arrayList, root);
        return arrayList;
    }

    public void pre(List list, TreeNode node) {
    
    
        if(node == null) {
    
    
            return;
        }
        list.add(node.val);
        pre(list, node.left);
        pre(list, node.right);
    }
}

时间复杂度O(n):n为节点个数,每个节点都会被访问一次且只会被访问一次

空间复杂度O(n):空间复杂度取决于递归的栈深度

非递归:

class Solution {
    
    
    public List<Integer> preorderTraversal(TreeNode root) {
    
    
        ArrayDeque<TreeNode> arrayDeque = new ArrayDeque<>();
        List<Integer> list = new ArrayList<>();
        if(root != null) {
    
    
            arrayDeque.push(root);
            while (!arrayDeque.isEmpty()) {
    
    
                TreeNode node = arrayDeque.pop();
                list.add(node.val);
                if(node.right != null) {
    
    
                    arrayDeque.push(node.right);
                }
                if(node.left != null) {
    
    
                    arrayDeque.push(node.left);
                }
            }
        }
        return list;
    }
}

2. 后序

递归:

class Solution {
    
    
    public List<Integer> postorderTraversal(TreeNode root) {
    
    
        List<Integer> arrayList = new ArrayList<>();
        last(arrayList, root);
        return arrayList;
    }

    public void last(List list, TreeNode node) {
    
    
        if(node == null) {
    
    
            return;
        }

        last(list, node.left);
        last(list, node.right);
        list.add(node.val);
    }
}

非递归:

class Solution {
    
    
    public List<Integer> postorderTraversal(TreeNode root) {
    
    
        ArrayDeque<TreeNode> arrayDeque = new ArrayDeque<>();
        ArrayDeque<TreeNode> stack = new ArrayDeque<>(); //辅助栈
        List<Integer> list = new ArrayList<>();
        if(root != null) {
    
    
            arrayDeque.push(root);
            while (!arrayDeque.isEmpty()) {
    
    
                TreeNode node = arrayDeque.pop();
                stack.push(node);

                if(node.left != null) {
    
    
                    arrayDeque.push(node.left);
                }
                if(node.right != null) {
    
    
                    arrayDeque.push(node.right);
                }
            }
        }
        while (!stack.isEmpty()) {
    
    
            list.add(stack.pop().val);
        }
        return list;
    }
}

3. 中序

递归:

class Solution {
    
    
    public List<Integer> inorderTraversal(TreeNode root) {
    
    
        List<Integer> arrayList = new ArrayList<>();
        if(root == null) {
    
    
            return arrayList; 
        }
        in(arrayList, root);
        return arrayList;
    }

    public void in(List list, TreeNode node) {
    
    
        if(node == null) {
    
    
            return;
        }
        in(list, node.left);
        list.add(node.val);
        in(list, node.right);
    }
}

非递归:

思路:

创建栈之后将整个左链添加到栈中,当左链的节点全部添加到栈之后,如果没有左子节点了,弹出的栈顶元素就是最后的结果,表示最左边节点的左子树中 “左根右” 的 “根”,也是整颗树的左,接下来将弹出节点的右子节点添加到栈中(如果存在),继续对此节点执行如上操作

image-20210518154247236

代码实现:

先想if-else,再考虑while循环。

class Solution {
    
    
    public List<Integer> inorderTraversal(TreeNode root) {
    
    
        List<Integer> res = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        //1. 栈的元素全部出栈,遍历结束,返回res
        //2. 由于第一个条件限定栈中有元素才开始遍历,而刚开始栈中是没有元素的,所以需要第二个条件保证遍历可以开始
        while(stack.size() > 0 || root != null) {
    
    
            //不断的向左走直到尽头,将遍历到的结点保存到栈中
            if(root != null) {
    
    
                stack.add(root);
                root = root.left;
            } else {
    
    
                //执行至此说明左边到尽头,此时应从栈中弹出元素,
                //保存到res中,去判断弹出结点的右子节点,持续此过程
                TreeNode tmp = stack.pop();
                res.add(tmp.val);
                root = tmp.right; //if条件会判断右子节点为空的情况,此处无需考虑
            }
        }
        return res;
    }
}

时间复杂度O(n):n为节点个数,每个节点都会被访问一次且只会被访问一次

空间复杂度O(n):空间复杂度取决于递归的栈深度

三、二叉树的层序遍历(102)

题目要求:

image-20210501142039436

在初级班知识点总结中题解的基础上,要将每层的节点数据单独的保存在一个List集合中,故要修改代码:

  • 首先队列中放入根节点,记录队列的元素个数,为1,表示这一层只有一个元素
  • 根节点出队列,根节点的的左孩子、右孩子入队,记录队列的元素个数,为2,表示这一层有两个元素
  • 所以最重要的是确定每层的节点个数,在节点弹出之前,此时队列中元素的个数就是本层节点的个数
  • ArrayDeque的两种方法:addXxx()、pollXxx()
class Solution {
    
    
    public List<List<Integer>> levelOrder(TreeNode root) {
    
    
        
        List<List<Integer>> res = new ArrayList<List<Integer>>(); //结果
        
        if(root == null) {
    
    
            return res; //防止root == null而出现空指针异常
        }
        
        ArrayDeque<TreeNode> arrayDeque = new ArrayDeque<>(); //队列
        arrayDeque.addFirst(root); //每次添加的节点都成为第一个节点,所以如果要当作队列来使用,需要每次从队尾取出节点
        
        while (!arrayDeque.isEmpty()) {
    
    
            
            //每层都需要创建一个新的List
            List<Integer> arrayList = new ArrayList<>();
            
            //记录本层的节点个数
            int currentLevelSize = arrayDeque.size();
            
            //对本层的所有节点执行循环
            for (int i = 1; i <= currentLevelSize; ++i) {
    
    
                
                TreeNode node = arrayDeque.pollLast();
                arrayList.add(node.val);
                
                if (node.left != null) {
    
    
                    arrayDeque.addFirst(node.left);
                }
                if (node.right != null) {
    
    
                    arrayDeque.addFirst(node.right);
                }
            }
            res.add(arrayList);
        }
        return res;
    }
}

时间复杂度与空间复杂度均为O(n)

四、二叉树的锯齿形层序遍历(103)

题目描述:

给定一个二叉树,返回其节点值的之字形层序遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

思路:

类似于之前的 “二叉树的层序遍历” 题目,唯一的区别在于弹出节点添加至某一层的LinkedList集合时,需要判断添加的顺序,可以使用双端队列表示某一层的LinkedList,从左往右时插入到尾部(默认双端队列中数据的输出顺序是从左往右输出,左边是头部),从右往左时插入到头部,再将此双端队列转换为LinkedList,即可将数据添加至List集合(结果集)。

判断插入顺序时可以使用一个变量 isOrderLeft ,为true时表示从左往右,为false时表示从右往左。

代码实现:

注意:

  1. ArrayDeque 只需要作为参数传递到 LinkedList 的构造器中,即可转换为List
  2. 弹出节点后添加元素的顺序仍然是左孩子 --> 右孩子
  3. 总的来说,只是改变了弹出节点添加到每层 LinkedList (ArrayDeque) 的顺序,其余步骤与上一题是一样的
class Solution {
    
    
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
    
    
        List<List<Integer>> res = new ArrayList<>();
        if (root == null) {
    
    
            return res;
        }

        //此队列用于放入元素弹出元素
        ArrayDeque<TreeNode> arrayDeque = new ArrayDeque<>();
        arrayDeque.addFirst(root);

        //判断插入顺序,初始值为true
        boolean isOrderLeft = true;

        while (!arrayDeque.isEmpty()) {
    
    

            //此双端队列用于按照不同的顺序添加节点
            ArrayDeque<Integer> deque = new ArrayDeque<>();

            int size = arrayDeque.size();

            for (int i = 0; i < size; i++) {
    
    
                
                //不用考虑弹出顺序,按照之前的思路即可
                //仅弹出元素之后此元素的插入方式与之前不同
                TreeNode node = arrayDeque.pollLast();

                //此时需要判断添加到双端队列的顺序
                if (isOrderLeft) {
    
    
                    //从左往右,插入到双端队列的尾部
                    deque.addLast(node.val);
                } else {
    
    
                    //从右往左,插入到双端队列的头部
                    deque.addFirst(node.val);
                }

                if (node.left != null) {
    
    
                    arrayDeque.addFirst(node.left);
                }
                if (node.right != null) {
    
    
                    arrayDeque.addFirst(node.right);
                }
            }

            //将双端队列转换成LinkedList
            List list = new LinkedList<Integer>(deque);
            res.add(list);

            //修改插入的顺序
            isOrderLeft = !isOrderLeft;
        }
        return res;
    }
}
image-20210527095919174

五、买卖股票的最佳时机(121)

题目描述:

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 ,如下图所示:

image-20210805234405756

注意:1. 卖出价格需要大于买入价格 2. 不能在买入前卖出股票

1. 暴力解法

思路:

使用双指针遍历数组,找出最大的差值。

代码实现:

public class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        
        //边界条件
        if (prices.length < 2) {
    
    
            return 0;
        }

        // 有可能不发生交易,因此结果集的初始值设置为0
        int res = 0;

        // 枚举所有发生交易的股价差
        for (int i = 0; i < prices.length - 1; i++) {
    
    
            for (int j = i + 1; j < prices.length; j++) {
    
    
                res = Math.max(res, prices[j] - prices[i]);
            }
        }
        return res;
    }
}
image-20210806000208584

2. 一次遍历

思路:

遍历数组,使用一个变量记录当前元素之前的最小值,再使用一个变量(结果值)记录最大值和最小值的差值。

如果发现当前元素小于等于之前的最小值,则当前元素成为最小值;如果当前元素大于之前的最小值,则记录二者的差值。

注意:如果每天都是历史新低,则返回0.

代码实现:

public class Solution {
    
    
    public int maxProfit(int prices[]) {
    
    

        //记录历史低值
        int minprice = Integer.MAX_VALUE;

        //最大差值,即返回值,默认为0
        int maxprofit = 0;

        //一次遍历
        for (int i = 0; i < prices.length; i++) {
    
    
            if (prices[i] <= minprice) {
    
    
                minprice = prices[i];

                //如果每天都是历史低值,则不会执行else if,返回0

                //一旦发现某一天的值大于历史最低且差值大于之前已经记录的最大差值,则记录新差值
            } else {
    
    
                maxprofit = Math.max(maxprofit, prices[i] - minprice);
            }
        }
        return maxprofit;
    }
}
image-20210806000643147

六、有效的括号(20)

题目描述:

给定一个只包括 ‘(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判断字符串是否有效,如:

image-20210510125618476

思路:

使用栈 + HashMap,每遇到一个左括号就将其入栈,每遇到一个右括号就拿栈顶元素与其比较,如果匹配则将栈顶元素移出,继续向后比较,如果不匹配,则返回false。需要使用map集合来进行栈顶元素与当前括号的比较:将三对括号放入map集合中,右括号为key,对应的左括号为value,遍历字符串遇到右括号时,map.get(右括号) == stack.pop()?

代码实现:

class Solution {
    
    
    public boolean isValid(String s) {
    
    
        
        //字符串为空,返回true
        if(s==null || "".equals(s)) return true;
        
        //字符串的长度为奇数,返回false
        int length = s.length();
        if (length % 2 != 0) return false;
        
        //放入左括号的栈
        Stack<Character> stack = new Stack<>();
        
        //定义map集合进行栈顶元素与当前右括号的比较
        HashMap<Character,Character> map = new HashMap<>();
        map.put(')','('); map.put(']','['); map.put('}','{');
        //注意添加的是字符,所以是单引号
        
        //遍历字符串,对左右括号进行不同的处理
        for(int i = 0; i < s.length(); i++) {
    
    
            char c = s.charAt(i);
            
            //判断得到的字符是左括号还是右括号
            
            if(!map.containsKey(c)) {
    
     //左括号
                stack.add(c);
            } else {
    
     //右括号
                
                //如果字符串第一个字符是右括号,直接false
                if(stack.size()==0) return false;
                
                //栈顶元素与当前右括号进行比较
                Character tmp = stack.pop();
                
                //栈顶元素与当前右括号不匹配,返回false
                if(map.get(c) != tmp) return false;
            }
        }
        
        //如果全是左括号就不为空
        //如果是有效的括号的话,栈中元素一定会被全部弹出
        return stack.empty();
    }
}
image-20210510130229725

七、字符串相加(415)

题目描述:

给定两个字符串形式的非负整数 num1num2 (字符串中的字符只包含0-9),计算它们的和,如下图所示:

image-20210526174957629

思路:

按照列竖式的方式计算和,如下图所示:

image-20210526175202055

使用两个指针,分别表示两个数的相同位数上的数字,再使用一个变量 add 表示进位的值。

两个指针 ij 一开始分别指向 nums1nums2 的末尾(最低位),从后向前遍历两个字符串的字符进行相加操作。

如果出现了两个字符串的字符个数不相等的情况,就判断某位置的索引是否小于0(第一个字符的索引为0),如果小于零,说明此位置没有字符,则对其赋予0值。

代码实现:

class Solution {
    
    
    public String addStrings(String num1, String num2) {
    
    
        
        //将i和j一开始分别定位在字符串的末尾位置,注意索引从0开始
        //进位add默认值为0
        int i = num1.length() - 1;
        int j = num2.length() - 1;
        int add = 0;
        
        //用来存放最终的结果
        StringBuffer ans = new StringBuffer();
        
        //当两数的最高位计算结束且最高位没有进位时停止计算
        while (i >= 0 || j >= 0 || add != 0) {
    
    
            
            //阿拉伯数字的ASCII码差值与数字本身的差值相同
            //如果索引小于0,说明此位置没有字符,当作0来处理
            int x = i >= 0 ? num1.charAt(i) - '0' : 0;
            int y = j >= 0 ? num2.charAt(j) - '0' : 0;
            
            //计算方式为两个位置的数以及进位相加
            int result = x + y + add;
            
            //除以10的余数作为两数相加的值
            ans.append(result % 10);
            //低位的值放在了最开头,所以最后需要将结果字符串反转
            
            //进位值只可能是0或1
            add = result / 10;
            
            //两个指针向前遍历
            i--;
            j--;
        }
        
        //计算完以后的答案需要翻转过来
        ans.reverse();
        return ans.toString();
    }
}
image-20210601104158787

八、合并两个有序数组(88)

题目描述:

给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使nums1 成为一个有序数组。

说明:

(1) 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。

(2) 你可以假设 nums1 有足够的空间 (空间大小大于或等于 m + n)来保存nums2 中的元素。如:

image-20210508125217654

需要了解的方法:

System类中:static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);

从指定源数组中将数据复制到某一个数组:

src:要从哪个数组复制元素

srcPos:被复制数组中从哪个位置开始复制元素

dest:要复制到哪个数组

destPos:从目标数组的哪个位置开始放入被复制的元素

length:要从被复制数组复制多少个元素

1. 合并后排序

思路:

将两个数组合并后再调用Arrays工具类的sort方法进行排序

代码实现:

class Solution {
    
    
    public void merge(int[] nums1, int m, int[] nums2, int n) {
    
    
        System.arraycopy(nums2, 0, nums1, m, n);
        Arrays.sort(nums1);
    }
}
image-20210508125345008

2. 双指针(从前往后)

思路:

定义一个大小为 m+n 的新数组,设定两个指针分别指向nums1数组与nums2数组的开头,在每一步将二者较小值放入新数组中,执行完毕后,如果某个数组还有剩余,则将剩余元素全部复制到新数组中,再把新数组的元素复制到nums1数组中(题目要求将结果放在nums1数组中)

因为需要创建一个大小为 m+n 的新数组,故空间复杂度:O(m+n)

代码实现:

class Solution {
    
    
    public void merge(int[] nums1, int m, int[] nums2, int n) {
    
    
        //定义一个可以存放m+n个元素的数组
        int[] res = new int[m + n]; //不能写大括号

        int p1 = 0; //nums1数组
        int p2 = 0; //nums2数组
        int p = 0; //res数组

        while (p1 < m && p2 < n) {
    
    
            res[p++] = nums1[p1] < nums2[p2] ? nums1[p1++] : nums2[p2++];
        }

        while (p1 < m) {
    
    
            res[p++] = nums1[p1++];
        }
        while (p2 < n) {
    
    
            res[p++] = nums2[p2++];
        }

        //因为要将结果保存在nums1中,所以需要将res的结果复制到nums1
        //题目说明
        System.arraycopy(res, 0, nums1, 0, m+n);
    }
}
image-20210508125443472

3. 双指针(从后往前)

思路:

在解法二中需要将nums1中的元素拷贝到新数组中,需要O(m)的空间复杂度,若从结尾开始从后往前向nums1中添加元素(nums1的长度大于等于n+m),此时不需要额外的空间。(注:从后往前时需将两个指针较大的值放入nums1中)

代码实现:

class Solution {
    
    
    public void merge(int[] nums1, int m, int[] nums2, int n) {
    
    
        
        int p1 = m - 1; //指向nums1要进行比较的数据的尾部
        int p2 = n - 1; //指向nums2的尾部
        int p = m + n - 1; //要在nums1中放入m+n个元素,故从m+n-1位置开始放入元素
        
        //p1与p2进行比较,将较大者从后向前放入nums1的尾部
        while ((p1 >= 0) && (p2 >= 0)) {
    
    
            nums1[p--] = (nums1[p1] < nums2[p2]) ? nums2[p2--] : nums1[p1--];
        }
        
        //正常情况下,nums1的前m+n个位置会被填满
        //如果p1>=0,说明nums2中的元素都大,且已经都放在了nums1中,而p1所要指向的剩余元素有序且已经在nums1中,故无需移动
        //如果p2>=0,说明p1所指向的元素大,这些元素已经在nums1中移动到合适的位置,故nums1前面的位置已经空出,需要将nums2中的剩余元素移动过去
        
        //将剩余的nums2的元素放入nums1
        while(p2 >= 0) {
    
    
            nums1[p--] = nums2[p2--];
        }
    }
}
image-20210508125527453

九、二分查找(704)

二分查找的使用前提是数组有序。

题目描述:

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

思路:

取中间值,比较,如果等于目标值则直接返回;如果目标值小于中间值,左边寻找;如果目标值大于中间值,右边寻找。

代码实现:

一定要注意什么时候用 else if,什么时候重新写一个 if,使用了 else if 的话只能进一个判断条件,其余判断都不会去执行了。

class Solution {
    
    
    public int search(int[] nums, int target) {
    
    
        
        //特殊条件判断
        int n = nums.length;
        if (n == 0) {
    
    
            return -1;
        }
        if (n == 1) {
    
    
            return nums[0] == target ? 0 : -1;
        }
        
        //定义二分查找的起止范围
        int l = 0;
        int r = nums.length - 1;
        while (l <= r) {
    
    
            int mid = l + ((r - l) >> 1);
            if (nums[mid] == target) {
    
    
                return mid;
            }
            
            //这里一定不能用else if,判断完中点一定要去判断两边
            
            if (target < nums[mid]) {
    
    
                r = mid - 1;
            } else {
    
     //这里可以使用else if,左右只选择一个执行即可
                l = mid + 1;
            }
        }
        return -1;
    }
}
  • 时间复杂度:O(logN)
  • 空间复杂度:O(1)

注意:可以通过遍历数组所有元素寻找目标值,但是时间复杂度会下降至O(n)。

十、搜索旋转排序数组(33)

题目描述:

整数数组 nums 按升序排列,数组中的值互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标从 0 开始计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你旋转后的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 ,如下图所示:

image-20210605212051710

思路:

对于有序数组查找一个值,可以使用二分查找。对于此题,旋转后是局部有序的,比如:

image-20210828174855264

按照二分法的思路,从中间值 7 分开以后数组变成了 [4, 5, 6] 和 [8, 1, 2, 3] 两个部分,其中左边 [4, 5, 6] 的数组是有序的,[8, 1, 2, 3] 是无序的,也就是说一个数组经过题目中这样的划分之后,一定有一边是有序的。

所以可以在常规二分查找的时候查看以当前 mid 为分割位置分割出来的两个部分 [l, mid] 和 [mid + 1, r] 哪个部分是有序的,并根据有序的那个部分确定二分查找的上下界,因为可以根据有序的性质,判断目标值在不在有序的范围内。

判断是否有序的方式:如果arr[l] <= arr[mid],说明左半部分有序,否则说明右半部分有序。

如下图所示:

image-20210605212736397
总结:将数组一分为二,其中一定有一个是有序的,另一个可能有序,也可能部分有序。
此时有序部分用二分法查找。无序部分再一分为二,其中一个一定有序,另一个可能有序,可能无序,有序部分使用二分法,就这样循环。

代码实现:

注意:此题一定要注意开闭区间。

class Solution {
    
    
    public int search(int[] nums, int target) {
    
    
        
        //特殊条件判断
        int n = nums.length;
        if (n == 0) {
    
    
            return -1;
        }
        if (n == 1) {
    
    
            return nums[0] == target ? 0 : -1;
        }
        
        //初始的左右区间
        int l = 0, r = n - 1;
        
        //二分查找
        while (l <= r) {
    
    
            
            int mid = l + ((r - l) >>> 1);
            
            if (nums[mid] == target) {
    
    
                return mid;
            }
            
            //以上全部都是二分查找的写法
            
            //中间值已经比较过了,所以之后的判断,中间值的那一边一定不会是闭区间
            
            //如果左部分是有序的
            //判断哪一部分是有序的,一定要带上等于号,小于等于中间值就说明左半部分有序,仅小于无法判断出来,比如左边全是55555这种,左半部分有序但等于中间值
            
            if (nums[l] <= nums[mid]) {
    
     //左部分是有序的
                
                //判断目标值在不在有序区间内,注意开闭区间
                if (nums[l] <= target && target < nums[mid]) {
    
    
                    r = mid - 1;
                } else {
    
    
                    l = mid + 1;
                }
            } else {
    
     //如果右部分是有序的
                
                //判断目标值在不在有序区间内,注意开闭区间
                if (nums[mid] < target && target <= nums[r]) {
    
    
                    l = mid + 1;
                } else {
    
    
                    r = mid - 1;
                }
            }
        }
        return -1;
    }
}
image-20210605212812207

十一、全排列(46)

题目描述:

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案,如下图:

image-20210828201429193

思路:

对于上述例子,一定是分情况讨论:

  1. 以1开头的情况

  2. 以2开头的情况

  3. 以3为开头的情况

所以可以使用for循环从头遍历数组的元素,每遍历到一个就将其与数组的首元素进行交换。剩下的元素再做全排列。

注意:

  1. 一定要确保每次的情况都是和首元素进行交换,也就是说每讨论完一种情况,就要将元素的位置还原回去,比如说遍历到2时,数组变成了 [2, 1, 3],当遍历到3时,应该在 [1, 2, 3] 的情况下和1交换,而不是在 [2, 1, 3] 的情况下去交换。
  2. 每有一种情况确定的时候 (剩余元素成为了单个元素的时候),都需要将整个数组的元素全部添加到临时的List集合中,再添加到最终的结果List集合中。

代码实现:

class Solution {
    
    
    public List<List<Integer>> permute(int[] nums) {
    
    
        //最终的结果
        List<List<Integer>> res = new ArrayList<List<Integer>>();

        backtrack(nums, 0, nums.length - 1, res);
        return res;
    }

    //参数p、q分别表示将要进行全排列的范围
    public void backtrack(int[] arr, int p, int q, List<List<Integer>> res) {
    
    

        //某一种情况讨论结束,应该将本次的结果保存起来
        if (p == q) {
    
    
            
            //每一种情况的临时list
            List<Integer> output = new ArrayList<Integer>();

            //将某次排列的结果(数组中的全部元素)添加到临时的list集合中
            for (int num : arr) {
    
    
                output.add(num);
            }
            res.add(new ArrayList<Integer>(output));
            
        } else {
    
    

            //开始遍历
            for (int i = p; i <= q; i++) {
    
    
                swap(arr, p, i);
                backtrack(arr, p + 1, q, res);
                //保证每次的情况都是和首元素进行交换,要将元素的位置还原回去
                swap(arr, p, i);
            }
        }
    }

    public void swap(int[] arr, int l, int r) {
    
    
        int temp = arr[l];
        arr[l] = arr[r];
        arr[r] = temp;
    }
}
  • 时间复杂度:O*(n×*n!),其中 n 为序列的长度
  • 空间复杂度:O(n)

猜你喜欢

转载自blog.csdn.net/weixin_49343190/article/details/122722075