剑指offer——java刷题总结【四】

Note

  • 题解汇总:剑指offer题解汇总
  • 代码地址:Github 剑指offer Java实现汇总
  • 点击目录中的题名链接可直接食用题解~
  • 有些解法博文中未实现,不代表一定很难,可能只是因为博主太懒```(Orz)
  • 如果博文中有明显错误或者某些题目有更加优雅的解法请指出,谢谢~

目录

题号题目名称
31从1到n整数中1出现的次数
32把数组排成最小的数
33丑数
34第一个只出现一次的字符
35数组中的逆序对
36两个链表的第一个公共节点
37数字在排序数组中出现的次数
38二叉树的深度
39平衡二叉树
40数组中只出现一次的数字

正文

31、从1到n整数中1出现的次数
题目描述

求出1~13的整数中1出现的次数,并算出100~1300的整数中1出现的次数?为此他特别数了一下1~13中包含1的数字有1、10、11、12、13因此共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。

题目分析

解法一:
以n=216为例:
个位上:1 ,11,21,31,…211。个位上共出现(216/10)+ 1个 1 。因为除法取整,210~216间个位上的1取不到,所以我们加8进位。你可能说为什么不加9,n=211怎么办,这里把最后取到的个位数为1的单独考虑,先往下看。
十位上:10~19,110~119,210~216。十位上可看成求(216/10)=21个位上的1的个数然后乘10。这里再次把最后取到的十位数为1的单独拿出来,即210~216要单独考虑 ,个数为(216%10)+1 .这里加8就避免了判断的过程。
后面以此类推。

m110100
a216212
b0616
count22+020+7100+0
代码实现

解法一: O(lgn)

public int NumberOf1Between1AndN_Solution(int n) {
    int count = 0;
    for (int m = 1; m <= n; m *= 10) {
        int a = n / m;
        int b = n % m;
        count += (a + 8) / 10 * m + (a % 10 == 1 ? b + 1 : 0);
    }
    return count;
}
32、把数组排成最小的数
题目描述

输入一个正整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。例如输入数组{3,32,321},则打印出这三个数字能排成的最小数字为321323。

题目分析

解法一: 贪心策略。自定义比较器,对a+b和b+a形成的数字进行比较。时间复杂度主要在排序。

代码实现

解法一: O(n²)

public String PrintMinNumber(int [] numbers) {
    String[] num = new String[numbers.length];
    for (int i = 0; i < numbers.length; i++) {
        num[i] = String.valueOf(numbers[i]);
    }
    Arrays.sort(num, new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return (o1 + o2).compareTo(o2 + o1);
        }
    });
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < num.length; i++) {
        sb.append(num[i]);
    }
    return sb.toString();
}
33、丑数
题目描述

把只包含质因子2、3和5的数称作丑数(Ugly Number)。例如6、8都是丑数,但14不是,因为它包含质因子7。 习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第N个丑数。

题目分析

解法一: 动态规划。dp[i] 表示第i个丑数,其状态转移方程如下:
dp[i] = min(2 * dp[dp2], 3 * dp[dp3], 5 * dp[dp5])
dp2、dp3和dp5都是指针,初始值指向dp[0]。由于丑数都是由2、3、5组成的,所以丑数都是由其它丑数乘以2、3、5组成的,我们初始化三个指针指向第一个丑数1,分别对它进行乘以2、3、5的操作,获取最小的那个数字作为新的丑数,并将对应的乘数+1,因为当前丑数作为乘数时已经被使用过了,应该使用更大的一个丑数作为下一次做乘法计算的乘数。时间复杂度:O(n) 空间复杂度:O(n)

求某个特殊的数时比如happy数、sad数都可以考虑使用这种解法,对该类数字从小到大进行扩充。

代码实现

解法一: O(n)

public int GetUglyNumber_Solution(int index) {
    if (index == 0) return 0;
    int[] num = new int[index + 1];
    num[1] = 1;
    int index2 = 1;
    int index3 = 1;
    int index5 = 1;
    for (int i = 2; i <= index; i++) {
        num[i] = Math.min(num[index2] * 2, num[index3] * 3);
        num[i] = Math.min(num[i], num[index5] * 5);
        if (num[i] == num[index5] * 5) {
            index5++;
        }
        if (num[i] == num[index3] * 3) {
            index3++;
        }
        if (num[i] == num[index2] * 2) {
            index2++;
        }
    }
    return num[index];
}
34、第一个只出现一次的字符
题目描述

在一个字符串(0<=字符串长度<=10000,全部由字母组成)中找到第一个只出现一次的字符,并返回它的位置, 如果没有则返回 -1(需要区分大小写)。

题目分析

解法一: 建立一个哈希表,第一次扫描的时候,统计每个字符的出现次数。第二次扫描的时候,如果该字符出现的次数为1,则返回这个字符的位置。时间复杂度O(n)

代码实现

解法一: O(n)

public int FirstNotRepeatingChar(String str) {
    if (str == null || str.length() == 0) return -1;
    Map<Character, Integer> map = new LinkedHashMap<>();
    char[] chars = str.toCharArray();
    for (int i = 0; i < chars.length; i++) {
        map.put(chars[i], map.getOrDefault(chars[i], 0) + 1);
    }
    for (int i = 0; i < chars.length; i++) {
        if (map.get(chars[i]) == 1) {
            return i;
        }
    }
    return -1;
}
35、数组中的逆序对
题目描述

在数组中的两个数字,如果前面一个数字大于后面的数字,则这两个数字组成一个逆序对。输入一个数组,求出这个数组中的逆序对的总数P。并将P对1000000007取模的结果输出。即输出P%1000000007。

输入描述

题目保证输入的数组中没有的相同的数字。
数据范围:
对于%50的数据,size<=10^4
对于%75的数据,size<=10^5
对于%100的数据,size<=2*10^5

题目分析

解法一: 使用归并排序的思想,在merge阶段做一下逆序对的判断,对count进行累加即可。

代码实现

解法一: O(n²)

public static int count = 0;
public static int InversePairs(int [] array) {
    if (array == null || array.length == 0) return 0;
    countSum(array, 0, array.length - 1);
    return count;
}

public static void countSum(int[] array, int L, int R) {
    if (L == R) {
        return;
    }
    int mid = L + ((R - L) >> 1);
    countSum(array, L, mid);
    countSum(array, mid + 1, R);
    merge(array, L, mid, R);
}

public static void merge(int[] array, int left, int mid, int right) {
    int[] help = new int[right - left + 1];
    int p1 = left;
    int p2 = mid + 1;
    int i = 0;
    while (p1 <= mid && p2 <= right) {
        count = array[p1] <= array[p2] ? (count % 1000000007) : (count + (mid - p1 + 1) % 1000000007);
        help[i++] = array[p1] <= array[p2] ? array[p1++] : array[p2++];
    }
    while (p1 <= mid) {
        help[i++] = array[p1++];
    }
    while (p2 <= right) {
        help[i++] = array[p2++];
    }
    for (i = 0; i < help.length; i++) {
        array[left + i] = help[i];
    }
}
36、两个链表的第一个公共节点
题目描述

输入两个链表,找出它们的第一个公共结点。

题目分析

解法一: 双指针法。创建两个指针p1和p2,分别指向两个链表的头结点,然后依次往后遍历。如果某个指针到达末尾,则将该指针指向另一个链表的头结点;如果两个指针所指的节点相同,则循环结束,返回当前指针指向的节点。比如两个链表分别为:1->3->4->5->6和2->7->8->9->5->6。短链表的指针p1会先到达尾部,然后重新指向长链表头部,当长链表的指针p2到达尾部时,重新指向短链表头部,此时p1在长链表中已经多走了k步(k为两个链表的长度差值),p1和p2位于同一起跑线,往后遍历找到相同节点即可。其实该方法主要就是用链表循环的方式替代了长链表指针先走k步这一步骤。

代码实现

解法一: O(m+n)

public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
    if (pHead1 == null || pHead2 == null) return null;
    ListNode p1 = pHead1;
    ListNode p2 = pHead2;
    while (p1 != p2) {
        p1 = p1 == null ? pHead2 : p1.next;
        p2 = p2 == null ? pHead1 : p2.next;
    }
    return p1;
}
37、数字在排序数组中出现的次数
题目描述

统计一个数字在排序数组中出现的次数。

题目分析

解法一: 数组有序用二分法查找该数字的某个出现位置,把时间复杂度压到logn,然后对查找到的index前后遍历累加相等值的出现次数即可。最终的时间复杂度和该数字的选择有关,大部分情况下都是在O(logn)内实现的,最坏情况为O(n)。不愿意自己手撸二分查找的,使用Arrays.binarySearch(arr, k)获得也可。

代码实现

解法一: O(n)

public int GetNumberOfK(int [] array , int k) {
    if (array.length == 0 || array == null) return 0;
    int left = 0;
    int right = array.length;
    int mid;
    int index = -1;
    while (left < right) {
        mid = left + ((right - left) >> 1);
        if (array[mid] < k) {
            left = mid + 1;
        } else if (array[mid] > k) {
            right = mid;
        } else {
            index = mid;
            break;
        }
    }
    if (index == -1) return 0;
    int count = 0;
    int t1 = index;
    int t2 = index + 1;
    while (t1 >= 0 && array[t1] == k) {
        count++;
        t1--;
    }
    while (t2 < array.length && array[t2] == k) {
        count++;
        t2++;
    }
    return count;
}
38、二叉树的深度
题目描述

输入一棵二叉树,求该树的深度。从根结点到叶结点依次经过的结点(含根、叶结点)形成树的一条路径,最长路径的长度为树的深度。

题目分析

解法一: 递归。其实子问题就是左子树高度和右子树高度之中的较大值加上当前节点,也就是高度+1。如果递归到的节点为空则返回0。每个节点只会访问一次,所以时间复杂度为O(n),空间开销为整棵递归树的高度logn。时间复杂度:O(n) 空间复杂度:O(logn)
解法二: 非递归。使用层序遍历的思想,利用队列,把每一层的节点都弹出后,在加入下一层的节点时count++,可以使用队列的size来判断当前层的节点是否都被弹出。空间开销是树中包含节点数最多的那一层的节点个数(大部分情况下应该是最后一层或者倒数第二层)。时间复杂度:O(n)

代码实现

解法一: O(n)

public int TreeDepth(TreeNode root) {
    if (root == null) return 0;
    return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1;
}

解法二: O(n)

public static int TreeDepth1(TreeNode root) {
    if (root == null) return 0;
    Queue<TreeNode> queue = new LinkedList<>();
    queue.add(root);
    int count = 0;
    int levelCount = 0;
    int qSize = queue.size();
    while (!queue.isEmpty()) {
        TreeNode t = queue.poll();
        levelCount++;
        if (t.left != null) queue.add(t.left);
        if (t.right != null) queue.add(t.right);
        if (levelCount == qSize) {
            levelCount = 0;
            qSize = queue.size();
            count++;
        }
    }
    return count;
}
39、平衡二叉树
题目描述

输入一棵二叉树,判断该二叉树是否是平衡二叉树。

题目分析

解法一: 递归。如果当前节点为null,也说明是平衡树,返回true。如果左子树和右子树的高度差大于1,说明以当前节点作为根节点的树不是平衡树,则直接返回false;如果左子树和右子树的高度差小于等于1,说明以当前节点作为根节点的树是平衡树,则递归判断对其左子树和右子树是否是平衡树。计算树的高度可参考上一题的解答。时间复杂度暂时还有点凌乱…(Master Method可计算递归的复杂度,需恶补)

代码实现

解法一:

public boolean IsBalanced_Solution(TreeNode root) {
    if (root == null) return true;
    if (Math.abs(TreeDepth(root.left) - TreeDepth(root.right)) > 1) {
        return false;
    } else {
        return IsBalanced_Solution(root.left) && IsBalanced_Solution(root.right);
    }
}

public int TreeDepth(TreeNode root) {
    if (root == null) return 0;
    return Math.max(TreeDepth(root.left), TreeDepth(root.right)) + 1;
}
40、数组中只出现一次的数字
题目描述

一个整型数组里除了两个数字之外,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。

题目分析

解法一: 哈希表。遍历数组,如果哈希表中存在该元素,说明该元素出现次数为2,则将该元素移除;如果哈希表中不存在该元素,则将该元素添加到哈希表。最后哈希表中留下的数字就是只出现一次数数字。
解法二: 位运算。遍历数组,将所有元素进行异或操作,由于异或相等为0,不等为1,因此最终的异或结果是只出现一次的数字a和b异或的结果r=a^b。然后我们可以基于r的二进制,根据第一位为1的位数,将原数组分为两组,分组的标准就是该位是否为1。相同的数字二进制数也一样,所以一定在一个组,而不同的数字肯定在不同的组,这样就能把a和b划分到两个不同组中。再对这两个组按照最开始的思路,分别求异或,最终的结果就是这两个只出现一次的数字。时间复杂度虽然一样,但是异或操作的指令完成速度更快。

代码实现

解法一: O(n)

public void FindNumsAppearOnce(int [] array,int num1[] , int num2[]) {
    if (array == null || array.length == 0) return;
    Set<Integer> set = new HashSet<>();
    for (int i = 0; i < array.length; i++) {
        if (!set.contains(array[i])) {
            set.add(array[i]);
        } else {
            set.remove(array[i]);
        }
    }
    Iterator<Integer> it = set.iterator();
    num1[0] = it.next();
    num2[0] = it.next();
}

解法二: O(n)

public void FindNumsAppearOnce1(int [] array,int num1[] , int num2[]) {
    if (array == null || array.length == 0) return;
    int xor = 0;
    for (int i = 0; i < array.length; i++) {
        xor ^= array[i];
    }
    int index = 1;
    while ((xor & index) == 0) {
        index <<= 1;
    }
    for (int i = 0; i < array.length; i++) {
        if ((array[i] & index) == 0) {
            num1[0] ^= array[i];
        } else {
            num2[0] ^= array[i];
        }
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值