Java的200道算法题,快来看看你会吗【其一】

1、合并两个有序数组

public class MergeSortedArrays {
    
    

    public static int[] merge(int[] arr1, int[] arr2) {
    
    
        int n = arr1.length;
        int m = arr2.length;
        int[] mergedArray = new int[n + m];
        int i = 0, j = 0, k = 0;

        // 遍历两个数组,将较小的元素放入新的数组中
        while (i < n && j < m) {
    
    
            if (arr1[i] < arr2[j]) {
    
    
                mergedArray[k++] = arr1[i++];
            } else {
    
    
                mergedArray[k++] = arr2[j++];
            }
        }

        // 如果有剩余的元素,直接复制到新数组中
        while (i < n) {
    
    
            mergedArray[k++] = arr1[i++];
        }

        while (j < m) {
    
    
            mergedArray[k++] = arr2[j++];
        }

        return mergedArray;
    }

    public static void main(String[] args) {
    
    
        int[] arr1 = {
    
    1, 3, 5, 7};
        int[] arr2 = {
    
    2, 4, 6, 8};

        int[] result = merge(arr1, arr2);

        // 打印合并后的数组
        for (int num : result) {
    
    
            System.out.print(num + " ");
        }
    }
}

2、移除元素

在Java中移除数组或集合中的元素,具体方法取决于你使用的数据结构。下面是针对不同数据结构的移除元素的方法:

数组

Java中的数组是固定大小的数据结构,因此不能直接从数组中移除元素。如果你想移除一个元素,通常需要创建一个新的数组(或者使用ArrayList),并将不需要移除的元素复制到新的数组中。

public class RemoveFromArray {
    
    
    public static int[] removeElement(int[] array, int index) {
    
    
        if (index < 0 || index >= array.length) {
    
    
            throw new IndexOutOfBoundsException("Index: " + index);
        }
        int[] newArray = new int[array.length - 1];
        for (int i = 0, j = 0; i < array.length; i++) {
    
    
            if (i != index) {
    
    
                newArray[j++] = array[i];
            }
        }
        return newArray;
    }

    public static void main(String[] args) {
    
    
        int[] array = {
    
    1, 2, 3, 4, 5};
        int[] result = removeElement(array, 2); // 移除索引为2的元素
        for (int num : result) {
    
    
            System.out.print(num + " ");
        }
    }
}

ArrayList

如果你使用的是ArrayList,那么可以直接调用remove方法来移除指定位置的元素或指定值的第一个匹配项。

import java.util.ArrayList;

public class RemoveFromArrayList {
    
    
    public static void main(String[] args) {
    
    
        ArrayList<String> list = new ArrayList<>();
        list.add("apple");
        list.add("banana");
        list.add("orange");

        // 移除指定位置的元素
        list.remove(1); // 移除"banana"

        // 移除指定值的第一个匹配项
        list.remove("orange"); // 移除"orange"

        for (String fruit : list) {
    
    
            System.out.println(fruit);
        }
    }
}

HashSet 和 LinkedHashSet

对于HashSetLinkedHashSet,你可以通过调用remove(Object o)方法来移除特定元素。

import java.util.HashSet;

public class RemoveFromHashSet {
    
    
    public static void main(String[] args) {
    
    
        HashSet<String> set = new HashSet<>();
        set.add("apple");
        set.add("banana");
        set.add("orange");

        // 移除指定元素
        set.remove("banana");

        for (String fruit : set) {
    
    
            System.out.println(fruit);
        }
    }
}

HashMap

对于HashMap,你可以通过调用remove(Object key)方法来移除特定键对应的键值对。

import java.util.HashMap;

public class RemoveFromHashMap {
    
    
    public static void main(String[] args) {
    
    
        HashMap<String, Integer> map = new HashMap<>();
        map.put("apple", 1);
        map.put("banana", 2);
        map.put("orange", 3);

        // 移除指定键的映射
        map.remove("banana");

        for (String key : map.keySet()) {
    
    
            System.out.println(key + ": " + map.get(key));
        }
    }
}

选择合适的数据结构可以让你更方便地进行元素的添加、移除等操作。如果你需要频繁地添加或移除元素,ArrayList可能是更好的选择,因为它提供了动态调整大小的功能。而如果需要保证元素的唯一性,并且不需要保持插入顺序,那么HashSet可能更适合。

3、删除有序数组中的重复项

在Java中删除有序数组中的重复项,可以利用数组已经排序的特点来简化操作。对于一个升序或降序排列的数组,相同的元素一定是相邻的。因此,我们可以通过遍历数组,并仅保留每个重复元素的第一个出现位置来去除重复项。

这里提供两种方法:一种是直接在原地移除重复项(适用于不需要保持原始数组的情况),另一种是创建一个新的数组来保存非重复元素(适用于需要保持原始数组不变的情况)。

方法1: 在原地移除重复项

这种方法修改了原始数组,但只需要O(1)的额外空间和O(n)的时间复杂度,其中n是数组的长度。

public class RemoveDuplicates {
    
    

    public static int removeDuplicates(int[] nums) {
    
    
        if (nums.length == 0) return 0;
        
        int uniqueIndex = 0; // 记录最后一个不重复元素的位置
        
        for (int i = 1; i < nums.length; i++) {
    
    
            // 如果当前元素与前一个元素不同,则不是重复项
            if (nums[i] != nums[uniqueIndex]) {
    
    
                uniqueIndex++;
                nums[uniqueIndex] = nums[i];
            }
        }
        
        // 返回的是新的数组长度
        return uniqueIndex + 1;
    }

    public static void main(String[] args) {
    
    
        int[] nums = {
    
    1, 1, 2, 2, 3, 4, 5, 5};
        int newLength = removeDuplicates(nums);
        for (int i = 0; i < newLength; i++) {
    
    
            System.out.print(nums[i] + " ");
        }
    }
}

方法2: 创建新数组保存非重复元素

这种方法不会改变原始数组,但是需要O(n)的额外空间来存储结果数组。

import java.util.Arrays;

public class RemoveDuplicatesWithNewArray {
    
    

    public static int[] removeDuplicates(int[] nums) {
    
    
        if (nums.length == 0) return new int[0];
        
        int uniqueCount = 1; // 至少有一个不同的元素
        
        for (int i = 1; i < nums.length; i++) {
    
    
            if (nums[i] != nums[i - 1]) {
    
    
                uniqueCount++;
            }
        }
        
        int[] result = new int[uniqueCount];
        result[0] = nums[0]; // 第一个元素总是唯一的
        
        int index = 1;
        for (int i = 1; i < nums.length; i++) {
    
    
            if (nums[i] != nums[i - 1]) {
    
    
                result[index++] = nums[i];
            }
        }
        
        return result;
    }

    public static void main(String[] args) {
    
    
        int[] nums = {
    
    1, 1, 2, 2, 3, 4, 5, 5};
        int[] result = removeDuplicates(nums);
        System.out.println(Arrays.toString(result));
    }
}

这两种方法都可以有效地从有序数组中移除重复项。选择哪种方法取决于你的具体需求,例如是否可以修改原始数组,以及你对空间复杂度的要求。

4、多数元素

在Java中找到数组中的多数元素(即出现次数超过数组长度一半的元素),可以使用多种算法。这里介绍两种常见的方法:摩尔投票算法(Boyer-Moore Voting Algorithm)和哈希表法。

方法1: 摩尔投票算法

摩尔投票算法是一个线性时间复杂度O(n)且常数空间复杂度O(1)的算法,适用于寻找一个数组中占多数的元素。该算法基于这样一个事实:如果一个元素出现次数超过数组长度的一半,那么它在与其他所有不同元素进行一对一抵消后仍然存在。

public class MajorityElement {
    
    

    public static int findMajority(int[] nums) {
    
    
        int count = 0;
        Integer candidate = null;

        for (int num : nums) {
    
    
            if (count == 0) {
    
    
                candidate = num;
            }
            count += (num == candidate) ? 1 : -1;
        }

        return candidate;
    }

    public static void main(String[] args) {
    
    
        int[] nums = {
    
    3, 2, 3};
        System.out.println("Majority element is: " + findMajority(nums));
    }
}

需要注意的是,摩尔投票算法假设输入数组确实存在一个多数元素。如果你不确定是否存在这样的元素,你可以在找到候选者之后再遍历一次数组以验证这个候选者是否真的是多数元素。

方法2: 使用哈希表

哈希表方法通过记录每个元素出现的次数来查找多数元素。这种方法的时间复杂度也是O(n),但需要额外的O(n)空间来存储哈希表。

import java.util.HashMap;

public class MajorityElementWithHashMap {
    
    

    public static int findMajority(int[] nums) {
    
    
        HashMap<Integer, Integer> countMap = new HashMap<>();
        int majorityCount = nums.length / 2;

        for (int num : nums) {
    
    
            countMap.put(num, countMap.getOrDefault(num, 0) + 1);
            if (countMap.get(num) > majorityCount) {
    
    
                return num;
            }
        }

        // 如果题目保证一定有解,这里不需要返回值。
        throw new IllegalArgumentException("No majority element found");
    }

    public static void main(String[] args) {
    
    
        int[] nums = {
    
    2, 2, 1, 1, 1, 2, 2};
        System.out.println("Majority element is: " + findMajority(nums));
    }
}

验证多数元素的存在

无论使用哪种方法,在实际应用中,如果你不能确定输入数组一定存在多数元素,你应该在找到候选多数元素后再次遍历数组来确认它确实是多数元素。这是因为摩尔投票算法仅能给出一个可能的多数元素,并不保证其正确性;而哈希表方法虽然能找到多数元素,但如果题目没有明确说明一定存在多数元素,则也需要验证。

选择哪种方法取决于你的具体需求,例如对时间和空间复杂度的要求,以及是否需要验证多数元素的存在。

5、轮转数组

在Java中轮转数组,可以通过多种方法实现。这里介绍两种常见的方式:使用额外的数组和就地轮转(不使用额外的空间)。此外,还有一种高效的算法叫做反转法,它可以在O(n)时间复杂度和O(1)空间复杂度下完成轮转。

方法1: 使用额外的数组

这种方法创建一个新的数组来保存轮转后的结果,然后将新数组复制回原数组。虽然简单易懂,但它需要额外的O(n)空间。

public class RotateArray {
    
    

    public static void rotate(int[] nums, int k) {
    
    
        if (nums == null || nums.length == 0) return;
        
        int n = nums.length;
        k = k % n; // 如果k大于数组长度,则取模
        
        if (k == 0) return;

        int[] result = new int[n];
        for (int i = 0; i < n; i++) {
    
    
            result[(i + k) % n] = nums[i];
        }
        
        System.arraycopy(result, 0, nums, 0, n);
    }

    public static void main(String[] args) {
    
    
        int[] nums = {
    
    1, 2, 3, 4, 5, 6, 7};
        rotate(nums, 3); // 轮转3个位置
        System.out.println(Arrays.toString(nums));
    }
}

方法2: 就地轮转

这种方法通过多次交换元素来实现轮转,不需要额外的空间,但是效率较低,尤其是当k接近数组长度时。

public class RotateArrayInPlace {
    
    

    public static void rotate(int[] nums, int k) {
    
    
        if (nums == null || nums.length == 0) return;
        
        int n = nums.length;
        k = k % n; // 如果k大于数组长度,则取模
        
        if (k == 0) return;

        for (int i = 0; i < k; i++) {
    
    
            int last = nums[n - 1];
            for (int j = n - 1; j > 0; j--) {
    
    
                nums[j] = nums[j - 1];
            }
            nums[0] = last;
        }
    }

    public static void main(String[] args) {
    
    
        int[] nums = {
    
    1, 2, 3, 4, 5, 6, 7};
        rotate(nums, 3); // 轮转3个位置
        System.out.println(Arrays.toString(nums));
    }
}

方法3: 反转法

这是最高效的方法之一,它通过三次反转来完成轮转:

  1. 反转整个数组。
  2. 反转前k个元素。
  3. 反转剩余的n-k个元素。

这种方法的时间复杂度为O(n),空间复杂度为O(1)。

import java.util.Arrays;

public class RotateArrayReverse {
    
    

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

    public static void rotate(int[] nums, int k) {
    
    
        if (nums == null || nums.length == 0) return;
        
        int n = nums.length;
        k = k % n; // 如果k大于数组长度,则取模
        
        if (k == 0) return;

        reverse(nums, 0, n - 1); // 反转整个数组
        reverse(nums, 0, k - 1); // 反转前k个元素
        reverse(nums, k, n - 1); // 反转剩余的元素
    }

    public static void main(String[] args) {
    
    
        int[] nums = {
    
    1, 2, 3, 4, 5, 6, 7};
        rotate(nums, 3); // 轮转3个位置
        System.out.println(Arrays.toString(nums));
    }
}

这三种方法都可以用来轮转数组。选择哪种方法取决于你的具体需求,例如对时间和空间复杂度的要求。对于大多数情况,推荐使用反转法,因为它不仅效率高,而且代码简洁。

6、买卖股票的最佳时机

"买卖股票的最佳时机"是LeetCode上的一个经典算法问题,它有不同的变体。我将向你介绍最基本的版本(LeetCode 121),即只允许进行一次交易(买入和卖出一支股票)来计算可以获得的最大利润。

问题描述

给定一个数组 prices,其中第 i 个元素是一支给定股票第 i 天的价格。如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。注意你不能在买入股票前卖出股票。

示例

  • 输入: [7,1,5,3,6,4]
  • 输出: 5
  • 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。

Java实现

这里是一个简单的Java实现:

class Solution {
    
    
    public int maxProfit(int[] prices) {
    
    
        if (prices == null || prices.length <= 1) {
    
    
            return 0;
        }
        
        int minPrice = Integer.MAX_VALUE; // 初始化最低价格为最大整数值
        int maxProfit = 0; // 初始化最大利润为0
        
        for (int price : prices) {
    
    
            if (price < minPrice) {
    
    
                minPrice = price; // 更新最低价格
            } else if (price - minPrice > maxProfit) {
    
    
                maxProfit = price - minPrice; // 更新最大利润
            }
        }
        
        return maxProfit;
    }
}

这段代码的时间复杂度是O(n),因为我们只需要遍历一次价格数组;空间复杂度是O(1),因为我们只使用了常量级的额外空间。

这个解决方案通过一次遍历数组来找到最小的购买价格,并且在每一天检查如果当天卖出能获得多少利润,同时保持最大利润的记录。最终返回的就是可以得到的最大利润。

7、跳跃游戏

跳跃游戏是一个经典的算法问题,通常在面试和技术挑战中出现。问题的基本形式如下:

问题描述:
给定一个非负整数数组 nums,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置能跳跃的最大长度。你的目标是判断是否能够到达最后一个位置。

示例:

输入: nums = [2,3,1,1,4]
输出: true
解释: 从位置0跳到位置1,然后跳跃3步到达最后一个位置。

输入: nums = [3,2,1,0,4]
输出: false
解释: 不管怎么跳,总会到达索引为3的位置,这里无法再向前移动。

解决方案思路:
一种常见的解法是从头开始遍历数组,并跟踪当前可以到达的最远距离(记作maxReach)。对于每个访问到的位置i,如果i小于或等于maxReach,则更新maxReach为max(maxReach, i + nums[i])。如果在任何时刻maxReach大于或等于最后一个位置的索引,那么就可以到达终点。如果遍历结束还没有达到最后一个位置,则不能到达。

实现代码示例(Java):

public class Solution {
    
    
    public boolean canJump(int[] nums) {
    
    
        int maxReach = 0;
        for (int i = 0; i < nums.length; i++) {
    
    
            // 如果当前位置超过了最大可达范围,则返回false
            if (i > maxReach) return false;
            // 更新最大可达范围
            maxReach = Math.max(maxReach, i + nums[i]);
            // 如果最大可达范围已经覆盖或超过了最后一个位置,提前返回true
            if (maxReach >= nums.length - 1) return true;
        }
        return false;
    }
}

这个算法的时间复杂度是O(n),其中n是数组的长度。空间复杂度是O(1),因为我们只使用了常数级的额外空间。这种解法非常高效,适用于大多数情况下的跳跃游戏问题。

8、H 指数

H指数(H-index)是用于衡量科学家、学者或研究人员的学术产出数量与质量的一个指标。它是由加利福尼亚大学圣地亚哥分校的物理学家Jorge E. Hirsch提出,因此也被称为Hirsch指数。

定义:
一个研究人员的H指数是指其发表的Np篇论文中有h篇每篇至少被引用了h次、而其他的(Np-h)篇论文每篇被引用次数不超过h次。

例如,如果一位作者的H指数为20,则意味着这位作者有20篇文章每篇都至少被引用了20次。

计算方法:
要计算一个人的H指数,可以按照以下步骤进行:

  1. 列出该人员所有论文,并按被引次数从高到低排序。
  2. 找出最大的数h,使得这h篇论文每篇都被引用了至少h次。

在编程中的应用:
H指数的概念也被应用于算法问题中,特别是处理数组和排序的问题。这类题目通常提供一个整数数组,其中每个元素代表某篇论文的引用次数,要求计算出相应的H指数。

下面是一个简单的Java代码示例来计算给定引用次数数组下的H指数:

import java.util.Arrays;

public class HIndexCalculator {
    
    
    public int hIndex(int[] citations) {
    
    
        Arrays.sort(citations);
        int h = 0, i = citations.length - 1;
        while (i >= 0 && citations[i] > h) {
    
    
            h++;
            i--;
        }
        return h;
    }

    public static void main(String[] args) {
    
    
        HIndexCalculator calculator = new HIndexCalculator();
        int[] citations = {
    
    3, 0, 6, 1, 5}; // 示例输入
        System.out.println("H指数是: " + calculator.hIndex(citations));
    }
}

这段代码首先对引用次数数组进行了排序,然后从最高引用次数开始遍历,逐个检查是否满足H指数的条件。这种方法的时间复杂度主要由排序决定,为O(n log n),其中n是数组的长度。对于一些特定情况,还可以使用计数排序等方法优化至线性时间复杂度。

9、O(1) 时间插入、删除和获取随机元素

要在 O(1) 时间复杂度内实现插入、删除和获取随机元素的功能,可以使用一个哈希表(HashMap)结合一个动态数组(ArrayList 或 Vector)。这种组合利用了哈希表的快速查找特性和数组的快速访问特性。

解决方案思路

  • 数据结构:使用一个哈希表和一个列表。哈希表存储每个值及其在列表中的索引位置;列表用于实际存储值。
  • 插入操作:将值添加到列表末尾,并在哈希表中记录该值及其对应的索引位置。时间复杂度为 O(1)。
  • 删除操作:为了保证删除操作也是 O(1),可以通过以下步骤:
    1. 从哈希表中找到待删除值的索引。
    2. 将列表末尾的元素移动到待删除值的位置,并更新哈希表中该元素的索引。
    3. 删除列表末尾的元素,并从哈希表中移除该值。整个过程保持 O(1) 的时间复杂度。
  • 获取随机元素:直接通过生成一个列表长度范围内的随机数来访问列表中的元素。时间复杂度为 O(1)。

Java 实现示例

import java.util.*;

class RandomizedSet {
    
    
    private Map<Integer, Integer> valToIndex = new HashMap<>();
    private List<Integer> values = new ArrayList<>();
    private Random rand = new Random();

    /** 插入操作 */
    public boolean insert(int val) {
    
    
        if (valToIndex.containsKey(val)) return false;
        valToIndex.put(val, values.size());
        values.add(val);
        return true;
    }

    /** 删除操作 */
    public boolean remove(int val) {
    
    
        if (!valToIndex.containsKey(val)) return false;
        int indexToRemove = valToIndex.get(val);
        int lastElement = values.get(values.size() - 1);

        // 将最后一个元素移到要删除的位置
        values.set(indexToRemove, lastElement);
        valToIndex.put(lastElement, indexToRemove);

        // 更新数据结构
        values.remove(values.size() - 1);
        valToIndex.remove(val);

        return true;
    }

    /** 获取随机元素 */
    public int getRandom() {
    
    
        return values.get(rand.nextInt(values.size()));
    }
}

这段代码展示了如何在一个集合中以 O(1) 的时间复杂度执行插入、删除和获取随机元素的操作。RandomizedSet 类提供了三个主要方法:insert, remove, 和 getRandom。通过巧妙地结合哈希表和列表,我们可以高效地完成这些操作。这种方法非常适合需要频繁进行上述三种操作的场景。

10、除自身以外数组的乘积

要解决“除自身以外数组的乘积”这个问题,我们需要生成一个输出数组 output,其中 output[i] 等于原数组 nums 中除了 i 位置元素之外所有元素的乘积。要求在不使用除法的情况下完成这一任务,并且以 O(n) 时间复杂度实现。

解决方案思路

我们可以分两步来计算这个结果:

  1. 从左到右遍历:对于每个元素,计算它左边所有元素的乘积。
  2. 从右到左遍历:对于每个元素,计算它右边所有元素的乘积,并与之前计算的左边乘积相乘。

这种方法确保了我们只需要两次线性扫描就可以得到最终结果,从而保证时间复杂度为 O(n),并且不需要使用除法。

实现步骤

  • 初始化一个数组 output 来保存结果。
  • 在第一次遍历中,从左到右填充 output 数组,使得 output[i] 包含 nums[0]nums[i-1] 的乘积。
  • 使用一个变量 rightProduct 来保存从右向左累积的乘积值。
  • 在第二次遍历中,从右向左更新 output 数组,将 output[i] 乘以 rightProduct,并更新 rightProduct 为当前索引右侧的所有元素乘积。

Java 实现示例

public class ProductExceptSelf {
    
    
    public int[] productExceptSelf(int[] nums) {
    
    
        int length = nums.length;
        int[] output = new int[length];
        
        // Initialize the result array with 1s.
        Arrays.fill(output, 1);
        
        int leftProduct = 1;
        for (int i = 0; i < length; i++) {
    
    
            output[i] = leftProduct;
            leftProduct *= nums[i];
        }
        
        int rightProduct = 1;
        for (int i = length - 1; i >= 0; i--) {
    
    
            output[i] *= rightProduct;
            rightProduct *= nums[i];
        }
        
        return output;
    }

    public static void main(String[] args) {
    
    
        ProductExceptSelf solution = new ProductExceptSelf();
        int[] nums = {
    
    1, 2, 3, 4};
        int[] result = solution.productExceptSelf(nums);
        System.out.println(Arrays.toString(result)); // 应输出 [24, 12, 8, 6]
    }
}

这段代码首先初始化了 output 数组,然后通过两次遍历(一次从左至右,一次从右至左)分别计算每个位置左侧和右侧的乘积。最终,output 数组中的每个元素即为除自身外所有其他元素的乘积。这样我们就能够以 O(n) 的时间复杂度解决问题,同时满足题目对空间复杂度的要求(不包括返回的结果数组)。