【データ構造とアルゴリズム】(24)高度なデータ構造とアルゴリズム設計:二つのポインタを用いた問題の解法と実装例

4.6 Leetcodeにおける二つのポインタ手法

以下の問題はすべて二つのポインタを用いるものであり、加えて以下のケースも含まれる:

  • Leetcode3: 最長の重複しない部分文字列(ハッシュテーブルの章で扱った)
  • ホーソーのクイックソート
  • 二分探索
  • など

ゼロの移動 - Leetcode 283

public class ZeroMoveLeetcode283 {
    static void moveZeros(int[] nums) {
        int left = 0;
        int right = 0;
        while (right < nums.length) {
            if (nums[right] != 0) {
                int temp = nums[left];
                nums[left] = nums[right];
                nums[right] = temp;
                left++;
            }
            right++;
        }
    }

    public static void main(String[] args) {
        int[] nums = {0, 1, 0, 3, 12};
        moveZeros(nums);
        System.out.println(Arrays.toString(nums));
    }
}

二数の和 II - Leetcode 167

public class TwoSumII {
    public static void main(String[] args) {
        System.out.println(Arrays.toString(twoSum(new int[]{2, 7, 11, 15}, 9)));
    }
    
    static public int[] twoSum(int[] numbers, int target) {
        return twoSum(numbers, 0, numbers.length - 1, target);
    }
    
    static int[] twoSum(int[] nums, int left, int right, int target) {
        int i = left;
        int j = right;
        while (i < j) {
            if (nums[i] + nums[j] < target) {
                i++;
            } else if (nums[i] + nums[j] > target) {
                j--;
            } else {
                break;
            }
        }
        return new int[]{i + 1, j + 1};
    }
}

Leetcode 1の二数の和との違いは、この問題では配列が昇順にソートされている点である。

三数の和 - Leetcode 15

public class ThreeSumLeetcode15 {

    static List<List<Integer>> threeSum(int[] nums) {
        Arrays.sort(nums);
        List<List<Integer>> result = new LinkedList<>();
        dfs(3, 0, nums.length - 1, 0, nums,
                new LinkedList<>(), result);
        return result;
    }

    static void dfs(int n, int i, int j, int target, int[] nums,
                    LinkedList<Integer> stack,
                    List<List<Integer>> result) {
        if (n == 2) {
            // 二数の和の問題を再利用
            twoSum(i, j, nums, target, stack, result);
            return;
        }
        for (int k = i; k < j - (n - 2); k++) {
            // 重複チェック
            if (k > i && nums[k] == nums[k - 1]) {
                continue;
            }
            // 一つの要素を固定し、残りのn-1個の和を求める
            stack.push(nums[k]);
            dfs(n - 1, k + 1, j, target - nums[k], nums, stack, result);
            stack.pop();
        }
    }

    static int count;

    static public void twoSum(int i, int j, int[] numbers, int target,
                              LinkedList<Integer> stack,
                              List<List<Integer>> result) {
        count++;
        while (i < j) {
            int sum = numbers[i] + numbers[j];
            if (sum < target) {
                i++;
            } else if (sum > target) {
                j--;
            } else { // 解を見つけた
                ArrayList<Integer> list = new ArrayList<>(stack);
                list.add(numbers[i]);
                list.add(numbers[j]);
                result.add(list);
                // 他の解を探す
                i++;
                j--;
                while (i < j && numbers[i] == numbers[i - 1]) {
                    i++;
                }
                while (i < j && numbers[j] == numbers[j + 1]) {
                    j--;
                }
            }
        }
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int[] candidates = {-4, -1, -1, 0, 0, 1, 1, 2};
        System.out.println("データサイズ:" + candidates.length);
        System.out.println(threeSum(candidates));
        System.out.println("処理時間:" + (System.currentTimeMillis() - start));
        System.out.println("再帰回数:" + count);
    }
}
  • この問題は以前の二数の和(Leetcode 1 および Leetcode 167)と異なる点は:
    • 二数の和では解答が一つだけであることが保証されているが、この問題ではすべての解答を求めなければならない
    • 重複を排除する必要がある
  • この問題は組み合わせ総和 II(Leetcode 40)と似ているが、以下のような違いがある:
    • Leetcode 40は任意の数の和がtargetになる組み合わせをすべて求めるが、この問題は三数の和がtargetになる組み合わせをすべて求める
    • Leetcode 40ではバックトラッキングを用いた場合の時間計算量はO(2^n * n)だが、この問題では再帰の深さが限られているため、時間計算量はO(n^2)となる
  • 最適化:固定する要素の位置を調整して、残りの要素が少なくとも2つ以上あるようにする。つまり、k < j - (n - 2)という条件を設ける

四数の和 - Leetcode 18

public class FourSumLeetcode18 {

    static List<List<Integer>> fourSum(int[] nums, int target) {
        Arrays.sort(nums);
        List<List<Integer>> result = new LinkedList<>();
        dfs(4, 0, nums.length - 1, target, nums,
                new LinkedList<>(), result);
        return result;
    }

    static void dfs(int n, int i, int j, int target, int[] nums,
                    LinkedList<Integer> stack,
                    List<List<Integer>> result) {
        if (n == 2) {
            // 二数の和の問題を再利用
            twoSum(i, j, nums, target, stack, result);
            return;
        }
        for (int k = i; k < j - (n - 2); k++) { // 四数の和では i < j-2、三数の和では i < j-1
            // 重複チェック
            if (k > i && nums[k] == nums[k - 1]) {
                continue;
            }
            // 一つの要素を固定し、残りのn-1個の和を求める
            stack.push(nums[k]);
            dfs(n - 1, k + 1, j, target - nums[k], nums, stack, result);
            stack.pop();
        }
    }

    static int count;

    static public void twoSum(int i, int j, int[] numbers, int target,
                              LinkedList<Integer> stack,
                              List<List<Integer>> result) {
        count++;
        while (i < j) {
            int sum = numbers[i] + numbers[j];
            if (sum < target) {
                i++;
            } else if (sum > target) {
                j--;
            } else { // 解を見つけた
                ArrayList<Integer> list = new ArrayList<>(stack);
                list.add(numbers[i]);
                list.add(numbers[j]);
                result.add(list);
                // 他の解を探す
                i++;
                j--;
                while (i < j && numbers[i] == numbers[i - 1]) {
                    i++;
                }
                while (i < j && numbers[j] == numbers[j + 1]) {
                    j--;
                }
            }
        }
    }

    public static void main(String[] args) {
        System.out.println(fourSum(new int[]{1, 0, -1, 0, -2, 2}, 0));
//        System.out.println(fourSum(new int[]{2, 2, 2, 2, 2}, 8));
//        System.out.println(fourSum(new int[]{1000000000,1000000000,1000000000,1000000000}, -294967296));
    }
}

最大の水量を持つ容器 - Leetcode 11

public class MaxWaterLeetcode11 {
    static int maxArea(int[] height) {
        int left = 0;
        int right = height.length - 1;
        int max = 0;
        while (left < right) {
            int minHeight = Integer.min(height[left], height[right]);
            max = Integer.max(max, (right - left) * minHeight);
            while (left < right && height[left] <= minHeight) {
                left++;
            }
            while (left < right && height[right] <= minHeight) {
                right--;
            }
        }
        return max;
    }

    public static void main(String[] args) {
        System.out.println(maxArea(new int[]{1, 8, 6, 2, 5, 4, 8, 3, 7})); // 49
        System.out.println(maxArea(new int[]{2,1})); // 1
    }
}

文字列の逆順配置 - Leetcode 344

二つのポインタを用いた解法

public class ReverseStringLeetcode344 {
    public static void main(String[] args) {
        char[] array = "abcde".toCharArray();
        reverseString(array);
        System.out.println(Arrays.toString(array));
    }

    static void reverseString(char[] s) {
        recursion(s, 0, s.length - 1);
    }

    public static void recursion(char[] array, int left, int right) {
        if (left >= right) {
            return;
        }
        swap(array, left, right);
        recursion(array, ++left, --right);
    }

    public static void swap(char[] array, int left, int right) {
        char temp = array[left];
        array[left] = array[right];
        array[right] = temp;
    }
}
  • 初回の交換はarray[0]とarray[4]の間で行われる
  • 2回目の交換はarray[1]とarray[3]の間で行われる
  • 3回目にはi=j=2となり、再帰が終了する
  • 配列の長さが偶数の場合、i>jになる時点で再帰が終了する

タグ: Java アルゴリズム データ構造 双指針 LeetCode

5月16日 07:47 投稿