刷leetCode算法题+解析(十六)

旋转数组

题目:给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。

示例 1:
输入: [1,2,3,4,5,6,7] 和 k = 3
输出: [5,6,7,1,2,3,4]
解释:
向右旋转 1 步: [7,1,2,3,4,5,6]
向右旋转 2 步: [6,7,1,2,3,4,5]
向右旋转 3 步: [5,6,7,1,2,3,4]
示例 2:
输入: [-1,-100,3,99] 和 k = 2
输出: [3,99,-1,-100]
解释:
向右旋转 1 步: [99,-1,-100,3]
向右旋转 2 步: [3,99,-1,-100]
说明:
尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。
要求使用空间复杂度为 O(1) 的 原地 算法。

思路:这个题乍一看让我想起了树的左旋右旋啊。不过这个题没有这么复杂其实,仔细看题就能发现所谓的右旋就是最右(后)位元素换到前面来了。不考虑性能的话暴力法还是很容易做到的。不管是挨个元素改变值,还是直接数组复制,反正还是那句话,实现简单,优化难。而且这个空间复杂度为O(1)也就是不额外建立数组,没要求时间复杂度。有点思路了,我先去撸代码
第一版本出炉:就是最暴力的解法,虽然满足空间复杂度,但是性能一言难尽。我再慢慢优化:

 public void rotate(int[] nums, int k) {
        for(int i = 0;i0;j--){
                nums[j] = nums[j-1];
            }
            nums[0] = last;
        }
    }

第二种方法就是数组倒转。用双指针的方法将数组倒转封装起来。然后这个
其实很容易理解,整个数组倒转。然后在k之前(包括k位)的再到转,k之后的倒转。
单纯这么说可能很抽象,画个图就好理解了。


三次倒转获得正确数据流程

反正情况就是这个情况,尽量脑补,接下来上代码:

class Solution {
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        k %= n;
        reverse(nums, 0, n - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, n - 1);
    }
    private void reverse(int[] nums, int first, int last) {
        while (first < last) {
            int temp = nums[first];
            nums[first++] = nums[last];
            nums[last--] = temp;
        }
    }
}

然后虽然题目要求三种方式,但是我觉得上种方法性能,时间已经很优了,所以就不再执着于方式了,这个题就到这里。如果感兴趣或者必须按照题目做出来的建议去看解析吧。附上大神提供的完整四种方法的思路。

import java.util.Arrays;

class Solution {
    /**
     * 双重循环
     * 时间复杂度:O(kn)
     * 空间复杂度:O(1)
     */
    public void rotate_1(int[] nums, int k) {
        int n = nums.length;
        k %= n;
        for (int i = 0; i < k; i++) {
            int temp = nums[n - 1];
            for (int j = n - 1; j > 0; j--) {
                nums[j] = nums[j - 1];
            }
            nums[0] = temp;
        }
    }

    /**
     * 翻转
     * 时间复杂度:O(n)
     * 空间复杂度:O(1)
     */
    public void rotate_2(int[] nums, int k) {
        int n = nums.length;
        k %= n;
        reverse(nums, 0, n - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, n - 1);
    }


    private void reverse(int[] nums, int start, int end) {
        while (start < end) {
            int temp = nums[start];
            nums[start++] = nums[end];
            nums[end--] = temp;
        }
    }

    /**
     * 循环交换
     * 时间复杂度:O(n)
     * 空间复杂度:O(1)
     */
    public void rotate_3(int[] nums, int k) {
        int n = nums.length;
        k %= n;
        // 第一次交换完毕后,前 k 位数字位置正确,后 n-k 位数字中最后 k 位数字顺序错误,继续交换
        for (int start = 0; start < nums.length && k != 0; n -= k, start += k, k %= n) {
            for (int i = 0; i < k; i++) {
                swap(nums, start + i, nums.length - k + i);
            }
        }
    }

    /**
     * 递归交换
     * 时间复杂度:O(n)
     * 空间复杂度:O(n/k)
     */
    public void rotate(int[] nums, int k) {
        // 原理同上
        recursiveSwap(nums, k, 0, nums.length);
    }

    private void recursiveSwap(int[] nums, int k, int start, int length) {
        k %= length;
        if (k != 0) {
            for (int i = 0; i < k; i++) {
                swap(nums, start + i, nums.length - k + i);
            }
            recursiveSwap(nums, k, start + k, length - k);
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

颠倒二进制位

题目:颠倒给定的 32 位无符号整数的二进制位。

示例 1:
输入: 00000010100101000001111010011100
输出: 00111001011110000010100101000000
解释: 输入的二进制串 00000010100101000001111010011100 表示无符号整数 43261596,
因此返回 964176192,其二进制表示形式为 00111001011110000010100101000000。
示例 2:
输入:11111111111111111111111111111101
输出:10111111111111111111111111111111
解释:输入的二进制串 11111111111111111111111111111101 表示无符号整数 4294967293,
因此返回 3221225471 其二进制表示形式为 10101111110010110010011101101001。
提示:
请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 2 中,输入表示有符号整数 -3,输出表示有符号整数 -1073741825。
进阶:
如果多次调用这个函数,你将如何优化你的算法?

思路:这个题。。我不知道是不是我没太理解,不就是将以此二进制串当做字符串然后完全倒转么?我再仔细审审题然后再来分享思路。
回来了,讲一下这道题的几个坑:1,说是输入二进制串,实际上输入的是int数。二,有个负数补码问题。我一开始的思路就卡在负数这一块了。
虽然我第一个思路是错的,但是这里也贴来上,知道为什么错才更好了解为什么对:

    public int reverseBits(int n) {
        StringBuffer sb = new StringBuffer();
        int i = 32;
        //因为是n是int,所以肯定是n先等于0,所以只要判断i就可以了
        while(i!=0 && n>=0){
            sb.append(n%2);
            n = n/2;
            i--;
        }
        return Long.valueOf(sb.toString(),2).intValue();
    }

这个就坑在如果是正数都ok,但是如果初始数字是负数则会报错。我是手动判断二进制位数的,初始是-1,然后还往sb上追加得出的结果根本不能看做是数字。所以这种生生拆数的方式就不行。
还能怎么获取二进制位数呢?位移。一种知道但是很少用的技术。
这里科普几个知识点:
按位与&。两个数字二进制按位对比,上下皆为1才是1,剩下都是0。
换言之任何数n&1.因为1前面位数都是0,只有最后一位才是1。所以得出的结果只能是1或者0.结果是1说明n的最后一位是1,得出结果是0说明n的最后一位是0。
左移<<。这个二进制串往左移动,最高位舍去,最低位补0。
右移>>和带符号右移>>>。普通右移是往右移动,低位舍去,高位补0。
而这个带符号右移是如果之前最高位是1,则低位舍去高位补1(保证移之前是负数移动后还是负数)如果最高位是0则和普通右移一样。
这道题的解法只需要用到这三种基础运算。
因为用上文用代码逻辑来获取一个数字的二进制只能保证整数的正确性,所以这里要用位移来实现。
大概步骤就是一个一个获取二进制的位数,然后直接放到应该放的位置(这个是有个反转的,比如获取的最后一位要放到结果的第一位,获取的倒数第二位放到结果的第二位)。
说了这么多直接上代码:

public class Solution {
    // you need treat n as an unsigned value
    public int reverseBits(int n) {
        //因为我第一版本用的while,所以不改了,这里其实while或者for都可以。
        int i = 0;
        int result = 0;
        //一共只有32位数。从0开始,所以到31就结束。
        while(i<32){
            //获取二进制串的最后一位是0是1.
            int temp = n&1;
            //依次放置的是前置位的数字,因为i最大是31,所以放置的数位最后是0位。
            result = result+(temp<<(31-i));
            //这个n每次右移一位,保证每次获取的temp是不断往上的位数。
            n = n>>1;
            i++;
        }
        return result;
    }
}

其实这道题感觉考得就是二进制的基础运算知识。我觉得我最大的错误就是主观解题,第一思路就不对(比如看了题目应该直接考虑用二进制解决问题,我还傻了吧唧的去字符串累加)。
其实位移用的真不多,所以这里下意识反应不对我也原谅自己了,顺便看了评论,说到了jdk中的二进制反转,人家的代码也只能叹为观止,因为我死活也没看懂。看了半天加上找了好多资料,还问了人,才勉强理解。。


image.png

我随便上个例子来讲:


demo

上图是我把jdk的方法源码拆开来一步一步运行的。其实乍一看规律挺不好找。但是只要知道原理就能发现规律了。
其实第一步就是一个两两交换的步骤。
从最后一位开始,最后一位和倒数第二位交换位置。倒数第三和倒数第四交换位置,倒数第五和倒数第六交换位置。。以此类推。如果到了第一个位数,前面没有可交换的了则补0。图上就是第一个1,再前面没数字了,补了个0,01交换为10.然後结束了。
第二步是一个两个一组两两交换。也是从最后两位开始。倒数第二和倒数第一整体和倒数第四,倒数第三这个整体交换。倒数第八第七和倒数第六第五交换。。一直到首位还是没有则补0。图上因为到了第一二位,前面没有了,所以补了两个0.也就是00 10两组交换位置,变成了10 00。
第三步也是这样,但是变成了四个四个一组两两交换,没有前面补0
第四步是以八为单位的交换,其实就是字符串翻转了(因为二进制中的八位数是十六进制中的一个数字)。
至此,整个翻转完成。


群友提供的交换流程

然後这个题就到这了(做题用半小时,研究jdk源码半小天儿~~)

但是我还是要说一个问题:那就是性能问题。上个题做出来不难,但是对于jdk源码理解起来很难,而且还费时间。但是最终一样的代码我在leetCode上运行了一下,jdk源码方法和我自己写的简单位移计算的方法运行起来执行速度相同,而且消耗也相同?



其实这个就挺有意思了,思路上不是一种方法实现的,但是性能是一样的。之前因为看不懂源码所以觉得应该就是厉害,但是现在有点坠下神坛的感觉。毕竟运行起来并没有多优秀吧?好吧,这是一个疑问,以后可能随着知道的越多就能理解了?下一题吧

位1的个数

编写一个函数,输入是一个无符号整数,返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。

示例 1:
输入:00000000000000000000000000001011
输出:3
解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 '1'。
示例 2:
输入:00000000000000000000000010000000
输出:1
解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 '1'。
示例 3:
输入:11111111111111111111111111111101
输出:31
解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 '1'。
提示:
请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。
进阶:
如果多次调用这个函数,你将如何优化你的算法?

思路:这道题就是上道题的变题,甚至比上题还要简单,没啥可说的了。上题的思路上修改一下就行了。

public class Solution {
    // you need to treat n as an unsigned value
    public int hammingWeight(int n) {
        int result = 0;
        for(int i=0;i<32;i++){
            int x = n&1;
            if(x==1){
                result++;
            }
            n = n>>1;
        }
        return result;
    }
}

一步步位移获取每一位上的数,如果是1则总数递加,不是就算了。简单明了。这道题过。

上升的温度

image.png

又做到一个sql题,但是因为这个题目让我学会了一个函数,所以在这列出来了,
日期的相减:dateDiff(date1,date2)结果是日期1减去日期2 的天数;
完整答案:

select w.Id from Weather w,Weather w1 
where  DateDiff(w.RecordDate,w1.RecordDate)=1 
and w.Temperature> w1.Temperature

今天就整理到这里,如果稍微帮到你了记得点个喜欢点个关注哟~每天进步一点点,也祝大家工作顺顺利利!

你可能感兴趣的:(刷leetCode算法题+解析(十六))