2024年面试大厂必背算法题

掌握以下即可面试

一、数组与字符串操作

1、找出数组中的最大值、最小值、第 K 大的元素。

a. 找出数组中的最大值

解题思路:遍历数组,维护一个当前最大值变量,比较每个元素与当前最大值,更新最大值。

public static int findMax(int[] arr) {
    int max = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

运行结果:

int[] nums = {3, 8, 2, 10, 5};
System.out.println("Max: " + findMax(nums)); // 输出:Max: 10

b. 找出数组中的最小值

解题思路:类似于找最大值,遍历数组,维护一个当前最小值变量,比较每个元素与当前最小值,更新最小值。

public static int findMin(int[] arr) {
    int min = arr[0];
    for (int i = 1; i < arr.length; i++) {
        if (arr[i] < min) {
            min = arr[i];
        }
    }
    return min;
}

运行结果:

int[] nums = {3, 8, 2, 10, 5};
System.out.println("Min: " + findMin(nums)); // 输出:Min: 2

c. 找出数组中的第 K 大的元素

解题思路:使用快速选择算法,类似于快速排序的分治思想,不断地将数组划分为左右两个部分,直到找到第 K 大的元素。

public static int findKthLargest(int[] arr, int k) {
    int left = 0, right = arr.length - 1;
    while (left <= right) {
        int pivotIdx = partition(arr, left, right);
        if (pivotIdx == k - 1) {
            return arr[pivotIdx];
        } else if (pivotIdx < k - 1) {
            left = pivotIdx + 1;
        } else {
            right = pivotIdx - 1;
        }
    }
    return -1; // 未找到
}

private static int partition(int[] arr, int left, int right) {
    int pivot = arr[right];
    int i = left;
    for (int j = left; j < right; j++) {
        if (arr[j] >= pivot) {
            swap(arr, i, j);
            i++;
        }
    }
    swap(arr, i, right);
    return i;
}

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

运行结果:

int[] nums = {3, 8, 2, 10, 5};
int k = 3;
System.out.println("3rd Largest: " + findKthLargest(nums, k)); // 输出:3rd Largest: 5

2、求数组中的两个数之和为特定值的下标。

解题思路

  1. 使用双重循环遍历数组中的每一对元素。
  2. 对于每一对元素,判断它们的和是否等于目标值。
  3. 如果找到了满足条件的一对元素,返回它们的下标。
public class TwoSum {

    // 定义一个方法,查找数组中两个数之和为特定值的下标
    public static int[] findTwoSum(int[] nums, int target) {
        int[] indices = new int[2];

        // 双重循环遍历数组中的每一对元素
        for (int i = 0; i < nums.length - 1; i++) {
            for (int j = i + 1; j < nums.length; j++) {
                if (nums[i] + nums[j] == target) {
                    // 找到满足条件的一对元素,记录它们的下标
                    indices[0] = i;
                    indices[1] = j;
                    return indices;
                }
            }
        }

        // 没有找到满足条件的一对元素,返回空数组
        return new int[0];
    }

    public static void main(String[] args) {
        int[] nums = {2, 7, 11, 15};
        int target = 9;

        // 调用 findTwoSum 方法,查找满足条件的下标对
        int[] result = findTwoSum(nums, target);

        if (result.length == 2) {
            System.out.println("Indices: " + result[0] + ", " + result[1]); // 输出:Indices: 0, 1
        } else {
            System.out.println("No solution found.");
        }
    }
}

3、判断一个字符串是否是回文串。

解题思路

  1. 使用双指针分别指向字符串的开头和末尾。
  2. 在每一步中,判断两个指针指向的字符是否相同。如果相同,将指针向内移动,继续比较下一个字符。
  3. 如果遇到不相同的字符,说明字符串不是回文串。
public class Palindrome {

    // 定义一个方法,判断字符串是否是回文串
    public static boolean isPalindrome(String str) {
        int left = 0;          // 左指针
        int right = str.length() - 1; // 右指针

        // 双指针法判断回文串
        while (left < right) {
            if (str.charAt(left) != str.charAt(right)) {
                return false; // 遇到不相同字符,不是回文串
            }
            left++; // 移动左指针向右
            right--; // 移动右指针向左
        }

        return true; // 字符串是回文串
    }

    public static void main(String[] args) {
        String str1 = "racecar";
        String str2 = "hello";

        // 调用 isPalindrome 方法,判断字符串是否是回文串
        System.out.println(str1 + " is palindrome: " + isPalindrome(str1)); // 输出:racecar is palindrome: true
        System.out.println(str2 + " is palindrome: " + isPalindrome(str2)); // 输出:hello is palindrome: false
    }
}

4、旋转数组:将数组向右旋转 k 步。

解题思路

  1. 将整个数组反转。
  2. 将前 k 个元素反转。
  3. 将剩余元素反转。
public class RotateArray {

    // 定义一个方法,将数组向右旋转 k 步
    public static void rotate(int[] nums, int k) {
        int n = nums.length;
        k %= n; // 处理 k 大于数组长度的情况

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

    // 定义一个方法,反转数组中指定范围的元素
    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 main(String[] args) {
        int[] nums = {1, 2, 3, 4, 5, 6, 7};
        int k = 3;

        // 调用 rotate 方法,将数组向右旋转 k 步
        rotate(nums, k);

        // 输出旋转后的数组
        for (int num : nums) {
            System.out.print(num + " ");
        }
        // 输出:5 6 7 1 2 3 4
    }
}

二、链表问题

1、反转链表。

解题思路

迭代法:使用三个指针 prev、current 和 next,每次将 current 的 next 指向prev,然后将三个指针依次向后移动一个位置,直到 current 为 null。

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
}

public class ReverseLinkedList {

    // 定义一个方法,迭代法反转链表
    public static ListNode reverseIterative(ListNode head) {
        ListNode prev = null;
        ListNode current = head;

        while (current != null) {
            ListNode next = current.next; // 保存当前节点的下一个节点
            current.next = prev;          // 当前节点的下一个节点指向 prev
            prev = current;               // prev 移动到当前节点
            current = next;               // current 移动到下一个节点
        }

        return prev; // 返回反转后的头节点
    }

    public static void main(String[] args) {
        // 创建一个链表:1 -> 2 -> 3 -> 4 -> 5
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);
        head.next.next.next.next = new ListNode(5);

        // 调用 reverseIterative 方法,迭代法反转链表
        ListNode reversedIterative = reverseIterative(head);
        printLinkedList(reversedIterative); // 输出:5 -> 4 -> 3 -> 2 -> 1
    }

    // 定义一个方法,输出链表的内容
    public static void printLinkedList(ListNode head) {
        while (head != null) {
            System.out.print(head.val + " -> ");
            head = head.next;
        }
        System.out.println("null");
    }
}

2、检测链表中是否存在环。

解题思路
使用两个指针,一个慢指针每次移动一步,一个快指针每次移动两步。如果链表中存在环,那么快指针最终会追上慢指针,如果链表没有环,快指针会提前到达链表尾部。

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
}

public class LinkedListCycle {

    // 定义一个方法,检测链表中是否存在环
    public static boolean hasCycle(ListNode head) {
        if (head == null || head.next == null) {
            return false; // 链表为空或只有一个节点,不存在环
        }

        ListNode slow = head; // 慢指针
        ListNode fast = head; // 快指针

        // 使用快慢指针法检测环
        // “fast.next != null” 防止报空指针异常
        while (fast != null && fast.next != null) {
            slow = slow.next;      // 慢指针移动一步
            fast = fast.next.next; // 快指针移动两步

            if (slow == fast) {
                return true; // 快慢指针相遇,说明链表存在环
            }
        }

        return false; // 快指针到达链表尾部,没有环
    }

    public static void main(String[] args) {
        // 创建一个链表:1 -> 2 -> 3 -> 4 -> 5 -> 2
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);
        head.next.next.next.next = new ListNode(5);
        head.next.next.next.next.next = head.next; // 尾节点连接到第二个节点

        // 调用 hasCycle 方法,检测链表中是否存在环
        boolean hasCycle = hasCycle(head);
        System.out.println("Has cycle: " + hasCycle); // 输出:Has cycle: true
    }
}

3、合并两个有序链表。

解题思路
迭代法:创建一个新的链表,遍历两个有序链表的节点,逐个比较节点的值,将较小的节点插入新链表中,然后将指针移动到下一个节点。

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
}

public class MergeTwoSortedLists {

    // 定义一个方法,迭代法合并两个有序链表
    public static ListNode mergeIterative(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1); // 创建一个虚拟头节点
        ListNode current = dummy; // 当前节点

        // 迭代比较两个链表的节点值,插入新链表
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                current.next = l1;
                l1 = l1.next;
            } else {
                current.next = l2;
                l2 = l2.next;
            }
            current = current.next;
        }

        // 将剩余的链表接到新链表末尾
        current.next = (l1 != null) ? l1 : l2;

        return dummy.next; // 返回新链表的头节点
    }

    public static void main(String[] args) {
        // 创建两个有序链表:1 -> 3 -> 5 和 2 -> 4 -> 6
        ListNode l1 = new ListNode(1);
        l1.next = new ListNode(3);
        l1.next.next = new ListNode(5);

        ListNode l2 = new ListNode(2);
        l2.next = new ListNode(4);
        l2.next.next = new ListNode(6);

        // 调用 mergeIterative 方法,迭代法合并两个有序链表
        ListNode mergedIterative = mergeIterative(l1, l2);
        printLinkedList(mergedIterative); // 输出:1 -> 2 -> 3 -> 4 -> 5 -> 6
    }

    // 定义一个方法,输出链表的内容
    public static void printLinkedList(ListNode head) {
        while (head != null) {
            System.out.print(head.val + " -> ");
            head = head.next;
        }
        System.out.println("null");
    }
}

4、删除链表中倒数第 N 个节点。

解题思路

  1. 使用两个指针 fast 和 slow,首先让 fast 指针先向前移动 N 步,然后同时移动 fast 和 slow 指针,直到fast 指针到达链表尾部。
  2. 此时 slow 指针指向倒数第 N + 1 个节点,即要删除的节点的前一个节点。
  3. 修改 slow 指针的 next 指针,跳过要删除的节点。
class ListNode {
    int val;
    ListNode next;
    ListNode(int val) { this.val = val; }
}

public class RemoveNthNodeFromEnd {

    // 定义一个方法,删除链表中倒数第 N 个节点
    public static ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1); // 创建一个虚拟头节点
        dummy.next = head;

        ListNode fast = dummy; // 快指针
        ListNode slow = dummy; // 慢指针

        // 先让 fast 指针向前移动 n 步
        for (int i = 0; i <= n; i++) {
            fast = fast.next;
        }

        // 同时移动 fast 和 slow 指针,直到 fast 到达链表尾部
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }

        // 修改 slow 指针的 next 指针,跳过要删除的节点
        slow.next = slow.next.next;

        return dummy.next; // 返回新链表的头节点
    }

    public static void main(String[] args) {
        // 创建一个链表:1 -> 2 -> 3 -> 4 -> 5
        ListNode head = new ListNode(1);
        head.next = new ListNode(2);
        head.next.next = new ListNode(3);
        head.next.next.next = new ListNode(4);
        head.next.next.next.next = new ListNode(5);

        int n = 2; // 要删除倒数第 n 个节点

        // 调用 removeNthFromEnd 方法,删除倒数第 n 个节点
        ListNode newHead = removeNthFromEnd(head, n);
        printLinkedList(newHead); // 输出:1 -> 2 -> 3 -> 5
    }

    // 定义一个方法,输出链表的内容
    public static void printLinkedList(ListNode head) {
        while (head != null) {
            System.out.print(head.val + " -> ");
            head = head.next;
        }
        System.out.println("null");
    }
}

三、树与图问题

1、二叉树的遍历(前序、中序、后序、层序)。

解题思路

  • 前序遍历:先访问根节点,然后递归遍历左子树,最后递归遍历右子树。
  • 中序遍历:先递归遍历左子树,然后访问根节点,最后递归遍历右子树。
  • 后序遍历:先递归遍历左子树,然后递归遍历右子树,最后访问根节点。
  • 层序遍历:使用队列来实现,从根节点开始,逐层将节点入队并出队,输出节点值。
class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
}

public class BinaryTreeTraversal {

    // 定义一个方法,前序遍历二叉树
    public static void preorderTraversal(TreeNode root) {
        if (root == null) {
            return;
        }
        System.out.print(root.val + " ");
        preorderTraversal(root.left);
        preorderTraversal(root.right);
    }

    // 定义一个方法,中序遍历二叉树
    public static void inorderTraversal(TreeNode root) {
        if (root == null) {
            return;
        }
        inorderTraversal(root.left);
        System.out.print(root.val + " ");
        inorderTraversal(root.right);
    }

    // 定义一个方法,后序遍历二叉树
    public static void postorderTraversal(TreeNode root) {
        if (root == null) {
            return;
        }
        postorderTraversal(root.left);
        postorderTraversal(root.right);
        System.out.print(root.val + " ");
    }

    // 定义一个方法,层序遍历二叉树
    public static void levelOrderTraversal(TreeNode root) {
        if (root == null) {
            return;
        }

        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            System.out.print(node.val + " ");

            if (node.left != null) {
                queue.offer(node.left);
            }
            if (node.right != null) {
                queue.offer(node.right);
            }
        }
    }

    public static void main(String[] args) {
        // 创建一个二叉树
        TreeNode root = new TreeNode(1);
        root.left = new TreeNode(2);
        root.right = new TreeNode(3);
        root.left.left = new TreeNode(4);
        root.left.right = new TreeNode(5);

        System.out.print("Preorder Traversal: ");
        preorderTraversal(root); // 输出:1 2 4 5 3

        System.out.print("\nInorder Traversal: ");
        inorderTraversal(root); // 输出:4 2 5 1 3

        System.out.print("\nPostorder Traversal: ");
        postorderTraversal(root); // 输出:4 5 2 3 1

        System.out.print("\nLevel Order Traversal: ");
        levelOrderTraversal(root); // 输出:1 2 3 4 5
    }
}

2、判断二叉树是否对称。

解题思路
一个二叉树是否对称取决于其左子树和右子树是否镜像对称。我们可以通过递归地比较左子树的左子树与右子树的右子树,以及左子树的右子树与右子树的左子树来判断。

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
}

public class SymmetricTree {

    // 定义一个方法,判断二叉树是否对称
    public static boolean isSymmetric(TreeNode root) {
        if (root == null) {
            return true;
        }
        return isMirror(root.left, root.right);
    }

    // 定义一个辅助方法,判断两棵树是否镜像对称
    private static boolean isMirror(TreeNode left, TreeNode right) {
        if (left == null && right == null) {
            return true;
        }
        if (left == null || right == null) {
            return false;
        }
        return (left.val == right.val)
                && isMirror(left.left, right.right)
                && isMirror(left.right, right.left);
    }

    public static void main(String[] args) {
        // 创建一个对称的二叉树
        TreeNode symmetricTree = new TreeNode(1);
        symmetricTree.left = new TreeNode(2);
        symmetricTree.right = new TreeNode(2);
        symmetricTree.left.left = new TreeNode(3);
        symmetricTree.left.right = new TreeNode(4);
        symmetricTree.right.left = new TreeNode(4);
        symmetricTree.right.right = new TreeNode(3);

        // 创建一个非对称的二叉树
        TreeNode asymmetricTree = new TreeNode(1);
        asymmetricTree.left = new TreeNode(2);
        asymmetricTree.right = new TreeNode(2);
        asymmetricTree.left.right = new TreeNode(3);
        asymmetricTree.right.right = new TreeNode(3);

        System.out.println("Symmetric Tree: " + isSymmetric(symmetricTree)); // 输出:Symmetric Tree: true
        System.out.println("Symmetric Tree: " + isSymmetric(asymmetricTree)); // 输出:Symmetric Tree: false
    }
}

3、在二叉树中查找路径和等于给定值的路径。

解题思路
在二叉树中查找路径和等于给定值的路径,可以使用深度优先搜索(DFS)递归遍历二叉树。从根节点开始,递归地向左子树和右子树搜索,同时更新当前路径的和。当搜索到叶子节点时,判断路径和是否等于给定值,如果相等,则将该路径添加到结果中。

class TreeNode {
    int val;
    TreeNode left;
    TreeNode right;
    TreeNode(int val) { this.val = val; }
}

public class PathSum {

    // 定义一个方法,查找路径和等于给定值的路径
    public static List<List<Integer>> findPathSum(TreeNode root, int targetSum) {
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> currentPath = new ArrayList<>();
        findPathSumHelper(root, targetSum, currentPath, result);
        return result;
    }

    // 定义一个辅助方法,递归查找路径和等于给定值的路径
    private static void findPathSumHelper(TreeNode node, int targetSum, List<Integer> currentPath, List<List<Integer>> result) {
        if (node == null) {
            return;
        }

        currentPath.add(node.val); // 添加当前节点到路径中
        targetSum -= node.val;     // 更新目标和

        if (node.left == null && node.right == null && targetSum == 0) {
            result.add(new ArrayList<>(currentPath)); // 找到一条路径,添加到结果中
        } else {
            findPathSumHelper(node.left, targetSum, currentPath, result);
            findPathSumHelper(node.right, targetSum, currentPath, result);
        }

        currentPath.remove(currentPath.size() - 1); // 移除当前节点,回溯
    }

    public static void main(String[] args) {
        // 创建一个二叉树
        TreeNode root = new TreeNode(5);
        root.left = new TreeNode(4);
        root.right = new TreeNode(8);
        root.left.left = new TreeNode(11);
        root.left.left.left = new TreeNode(7);
        root.left.left.right = new TreeNode(2);
        root.right.left = new TreeNode(13);
        root.right.right = new TreeNode(4);
        root.right.right.left = new TreeNode(5);
        root.right.right.right = new TreeNode(1);

        int targetSum = 22;

        List<List<Integer>> paths = findPathSum(root, targetSum);
        for (List<Integer> path : paths) {
            System.out.println("Path: " + path); // Path: [5, 4, 11, 2]、Path: [5, 8, 4, 5]
        }
    }
}

4、图的遍历(深度优先搜索、广度优先搜索)。

深度优先搜索(DFS):
从起始节点开始,递归地访问当前节点,并将其标记为已访问。
对于当前节点的每个邻接节点,如果邻接节点未被访问过,则递归访问邻接节点。

广度优先搜索(BFS):
从起始节点开始,将其入队,并标记为已访问。
不断从队列中取出一个节点,并将其邻接节点入队,直到队列为空。

import java.util.*;

public class GraphTraversal {

    static class Graph {
        private int vertices;
        private List<List<Integer>> adjList;

        Graph(int vertices) {
            this.vertices = vertices;
            adjList = new ArrayList<>();
            for (int i = 0; i < vertices; i++) {
                adjList.add(new ArrayList<>());
            }
        }

        // 添加图的边
        void addEdge(int source, int destination) {
            adjList.get(source).add(destination);
        }

        // 深度优先搜索遍历
        void dfs(int start) {
            boolean[] visited = new boolean[vertices];
            dfsHelper(start, visited);
        }

        private void dfsHelper(int node, boolean[] visited) {
            visited[node] = true; // 标记当前节点为已访问
            System.out.print(node + " "); // 输出当前节点

            // 递归访问邻接节点
            for (int neighbor : adjList.get(node)) {
                if (!visited[neighbor]) {
                    dfsHelper(neighbor, visited);
                }
            }
        }

        // 广度优先搜索遍历
        void bfs(int start) {
            boolean[] visited = new boolean[vertices];
            Queue<Integer> queue = new LinkedList<>();

            visited[start] = true;
            queue.offer(start);

            while (!queue.isEmpty()) {
                int node = queue.poll();
                System.out.print(node + " "); // 输出当前节点

                // 将邻接节点入队
                for (int neighbor : adjList.get(node)) {
                    if (!visited[neighbor]) {
                        visited[neighbor] = true;
                        queue.offer(neighbor);
                    }
                }
            }
        }
    }

    public static void main(String[] args) {
        int vertices = 7;
        Graph graph = new Graph(vertices);

        graph.addEdge(0, 1);
        graph.addEdge(0, 2);
        graph.addEdge(1, 3);
        graph.addEdge(1, 4);
        graph.addEdge(2, 5);
        graph.addEdge(2, 6);

        int startNode = 0;

        System.out.print("DFS Traversal: ");
        graph.dfs(startNode); // 输出:DFS Traversal: 0 1 3 4 2 5 6

        System.out.print("\nBFS Traversal: ");
        graph.bfs(startNode); // 输出:BFS Traversal: 0 1 2 3 4 5 6
    }
}

四、排序与搜索

1、快速排序、归并排序等各类排序算法。

a.快速排序

解题思路:
快速排序是一种基于分治思想的排序算法。它的基本思想是选取一个基准元素,然后将数组分割为两部分,一部分小于基准元素,一部分大于基准元素。然后递归地对两部分进行排序。

public class QuickSort {

    /**
     * 快速排序入口函数
     *
     * @param arr  待排序数组
     * @param low  左边界索引
     * @param high 右边界索引
     */
    public static void quickSort(int[] arr, int low, int high) {
        if (low < high) {
            // 进行分区操作,获取分界点的索引
            int partitionIndex = partition(arr, low, high);
            // 对分界点左右两部分进行递归排序
            quickSort(arr, low, partitionIndex - 1);
            quickSort(arr, partitionIndex + 1, high);
        }
    }

    /**
     * 分区操作,将数组分为小于基准和大于基准的两部分
     *
     * @param arr  待分区数组
     * @param low  左边界索引
     * @param high 右边界索引
     * @return 分界点的索引
     */
    private static int partition(int[] arr, int low, int high) {
        int pivot = arr[high];  // 选择最右边的元素作为基准
        int i = low - 1;  // i表示小于基准的部分的最后一个元素的索引

        // 遍历数组,将小于基准的元素交换到左边
        for (int j = low; j < high; j++) {
            if (arr[j] < pivot) {
                i++;
                swap(arr, i, j);
            }
        }

        // 将基准元素交换到正确的位置
        swap(arr, i + 1, high);
        return i + 1;
    }

    /**
     * 交换数组中两个元素的位置
     *
     * @param arr 数组
     * @param i   第一个元素索引
     * @param j   第二个元素索引
     */
    private static void swap(int[] arr, int i, int j) {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    public static void main(String[] args) {
        int[] arr = { 7, 2, 1, 6, 8, 5, 3, 4 };
        quickSort(arr, 0, arr.length - 1);

        // 打印排序结果
        for (int num : arr) {
            System.out.print(num + " "); //排序结果:1 2 3 4 5 6 7 8
        }
    }
}

a.归并排序

public class MergeSort {

    /**
     * 归并排序入口函数
     *
     * @param arr   待排序数组
     * @param left  左边界索引
     * @param right 右边界索引
     */
    public static void mergeSort(int[] arr, int left, int right) {
        if (left < right) {
            int mid = left + (right - left) / 2;
            // 分别对左右两部分进行归并排序
            mergeSort(arr, left, mid);
            mergeSort(arr, mid + 1, right);
            // 合并两部分
            merge(arr, left, mid, right);
        }
    }

    /**
     * 合并两个有序数组
     *
     * @param arr   待合并数组
     * @param left  左边界索引
     * @param mid   中间索引
     * @param right 右边界索引
     */
    private static void merge(int[] arr, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1, k = 0;

        // 将两个有序部分合并为一个有序部分
        while (i <= mid && j <= right) {
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            } else {
                temp[k++] = arr[j++];
            }
        }

        // 处理剩余元素
        while (i <= mid) {
            temp[k++] = arr[i++];
        }

        while (j <= right) {
            temp[k++] = arr[j++];
        }

        // 将合并后的数组拷贝回原数组
        for (int p = 0; p < temp.length; p++) {
            arr[left + p] = temp[p];
        }
    }

    public static void main(String[] args) {
        int[] arr = { 7, 2, 1, 6, 8, 5, 3, 4 };
        mergeSort(arr, 0, arr.length - 1);

        // 打印排序结果
        for (int num : arr) {
            System.out.print(num + " "); // 排序结果:1 2 3 4 5 6 7 8
        }
    }
}

2、二分查找:在有序数组中查找特定元素。

解题思路

  1. 首先,确定搜索范围的起始和结束位置,即数组的左边界和右边界。
  2. 使用循环或递归来执行二分查找,每次取中间元素。
  3. 如果中间元素等于目标元素,则返回其索引。
  4. 如果中间元素大于目标元素,说明目标元素可能在左半部分,缩小搜索范围到左半部分。
  5. 如果中间元素小于目标元素,说明目标元素可能在右半部分,缩小搜索范围到右半部分。
  6. 重复步骤2-5,直到找到目标元素或搜索范围为空。
public class BinarySearch {

    // 二分查找函数
    public static int binarySearch(int[] arr, int target) {
        int left = 0;
        int right = arr.length - 1;

        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (arr[mid] == target) {
                return mid; // 找到目标元素,返回索引
            } else if (arr[mid] < target) {
                left = mid + 1; // 在右半部分继续搜索
            } else {
                right = mid - 1; // 在左半部分继续搜索
            }
        }

        return -1; // 没找到目标元素
    }

    public static void main(String[] args) {
        int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        int target = 6;
        int result = binarySearch(arr, target);

        if (result != -1) {
            System.out.println("目标元素 " + target + " 的索引是:" + result);  // 目标元素 6 的索引是:5	
        } else {
            System.out.println("目标元素 " + target + " 未找到。");
        }
    }
}

3、查找旋转排序数组中的最小值。

解题思路
由于数组是旋转有序的,我们可以使用二分查找的变种来寻找最小值。在二分查找中,我们可以通过比较中间元素和左右两端元素的大小关系,判断最小值在哪一侧,然后缩小搜索范围。

  1. 首先,我们初始化左右指针分别指向数组的左右边界。
  2. 使用循环,不断缩小搜索范围,直到左指针超过右指针。
  3. 在每一轮循环中,找到中间索引,比较中间元素和左右指针对应的元素,判断最小值在哪一侧。
  4. 更新左右指针的位置。
  5. 当左指针超过右指针时,返回左指针对应的元素作为最小值。
public class FindMinimumInRotatedSortedArray {

    public static int findMin(int[] nums) {
        int left = 0;
        int right = nums.length - 1;

        while (left < right) {
            int mid = left + (right - left) / 2;

            // 如果中间元素大于右指针对应元素,说明最小值在右侧
            if (nums[mid] > nums[right]) {
                left = mid + 1;
            }
            // 如果中间元素小于等于右指针对应元素,说明最小值在左侧或就是中间元素
            else {
                right = mid;
            }
        }

        // 当循环结束时,左指针指向最小值
        return nums[left];
    }

    public static void main(String[] args) {
        int[] nums = { 4, 5, 6, 7, 0, 1, 2 };
        int min = findMin(nums);
        System.out.println("旋转排序数组的最小值是:" + min); // 旋转排序数组的最小值是:0
    }
}

4、字符串搜索:在字符串中查找特定子串。

解题思路
暴力搜索算法的基本思想是,从主串的每一个位置开始,逐个比较主串和子串的字符,如果匹配失败,就移动主串的指针,重新开始比较。

  1. 遍历主串的每一个位置,作为比较的起始点。
  2. 对于每一个起始点,逐个比较主串和子串的字符。
  3. 如果字符不匹配,主串指针和子串指针都回溯到起始点的下一个位置,继续下一轮比较。
  4. 如果比较成功,返回子串在主串中的起始位置。
public class StringSearch {

    public static int searchString(String mainStr, String subStr) {
        int mainLen = mainStr.length();
        int subLen = subStr.length();

        for (int i = 0; i <= mainLen - subLen; i++) {
            int j = 0;
            while (j < subLen && mainStr.charAt(i + j) == subStr.charAt(j)) {
                j++;
            }
            if (j == subLen) {
                return i; // 找到匹配的子串,返回起始位置
            }
        }

        return -1; // 没找到匹配的子串
    }

    public static void main(String[] args) {
        String mainStr = "This is a sample text for searching.";
        String subStr = "sample";
        int result = searchString(mainStr, subStr);

        if (result != -1) {
            System.out.println("子串在主串中的起始位置是:" + result); // 子串在主串中的起始位置是:10
        } else {
            System.out.println("子串未找到。");
        }
    }
}

五、动态规划

1、斐波那契数列。

解题思路
斐波那契数列是一个递归定义的数列,其中每个数都是前两个数之和。
使用迭代方式:从前往后逐个计算斐波那契数,利用两个变量保存前两个数。

public class Fibonacci {

    // 使用迭代方式计算斐波那契数
    public static int iterativeFibonacci(int n) {
        if (n <= 1) {
            return n;
        }
        
        int prev1 = 0;
        int prev2 = 1;
        int current = 0;
        
        for (int i = 2; i <= n; i++) {
            current = prev1 + prev2;
            prev1 = prev2;
            prev2 = current;
        }
        
        return current;
    }

    public static void main(String[] args) {
        int n = 10;
        
        System.out.println("使用迭代方式计算斐波那契数:");
        for (int i = 0; i <= n; i++) {
            System.out.print(iterativeFibonacci(i) + " ");
        }
        System.out.println();
        // 使用迭代方式计算斐波那契数:
		// 0 1 1 2 3 5 8 13 21 34 55 
    }
}

2、背包问题:0-1 背包、完全背包等。

a. 0-1背包问题

0-1背包问题是指在有限的背包容量下,选择一些物品放入背包,使得物品的总价值最大,每种物品只能选择放或不放。

解题思路
使用动态规划的思想,创建一个二维数组dp,其中dp[i][j]表示前i个物品在背包容量为j时的最大价值。

  1. 初始化dp数组,dp[0][j]表示前0个物品在背包容量为j时的最大价值都为0。
  2. 对于每个物品i,如果当前背包容量j小于物品i的重量,则dp[i][j] = dp[i-1][j],即不放入物品i。
  3. 否则,dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]),即选择放入物品i或不放入物品i的最大价值。
public class Knapsack01 {

    public static int knapsack01(int[] weights, int[] values, int capacity) {
        int n = weights.length;
        // 创建一个二维数组dp,其中dp[i][j]表示前i个物品在背包容量为j时的最大价值
        int[][] dp = new int[n + 1][capacity + 1];

        // 动态规划过程
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= capacity; j++) {
                if (j < weights[i - 1]) {
                    // 如果当前物品的重量大于背包容量,无法放入背包
                    dp[i][j] = dp[i - 1][j];
                } else {
                    // 选择放入物品i或不放入物品i的最大价值
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
                }
            }
        }

        return dp[n][capacity];
    }

    public static void main(String[] args) {
        int[] weights = {2, 3, 4, 5};
        int[] values = {3, 4, 5, 6};
        int capacity = 5;

        int maxValue = knapsack01(weights, values, capacity);
        System.out.println("0-1背包问题的最大价值:" + maxValue); // 0-1背包问题的最大价值:7
    }
}

b. 完全背包问题

完全背包问题是指在有限的背包容量下,选择一些物品放入背包,使得物品的总价值最大,每种物品可以选择放入多次。

解题思路
使用动态规划的思想,创建一个一维数组dp,其中dp[j]表示背包容量为j时的最大价值。

  1. 初始化dp数组,dp[0]表示背包容量为0时的最大价值为0。
  2. 对于每个物品i,遍历背包容量j从weights[i]到capacity,更新dp[j]为max(dp[j], dp[j -
    weights[i]] + values[i])。
public class CompleteKnapsack {

    public static int completeKnapsack(int[] weights, int[] values, int capacity) {
        int n = weights.length;
        // 创建一个一维数组dp,其中dp[j]表示背包容量为j时的最大价值
        int[] dp = new int[capacity + 1];

        // 动态规划过程
        for (int i = 0; i < n; i++) {
            for (int j = weights[i]; j <= capacity; j++) {
                // 更新背包容量为j时的最大价值
                dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
            }
        }

        return dp[capacity];
    }

    public static void main(String[] args) {
        int[] weights = {2, 3, 4, 5};
        int[] values = {3, 4, 5, 6};
        int capacity = 10;

        int maxValue = completeKnapsack(weights, values, capacity);
        System.out.println("完全背包问题的最大价值:" + maxValue); // 完全背包问题的最大价值:19
    }
}

3、最长递增子序列。

解题思路:

  1. 创建一个与原序列长度相同的数组 lisLengths,用于存储以每个位置为结尾的最长递增子序列的长度。
  2. 初始化 lisLengths 数组的所有元素为1,因为每个单独的元素本身就是一个递增子序列。
  3. 遍历原序列,对于每个元素,检查其前面的元素,如果前面的元素小于当前元素,则更新 lisLengths 数组中的值为前面元素的最长递增子序列长度加1。
  4. 同时,记录最长递增子序列的长度和最后一个元素的位置。
  5. 从最长递增子序列的最后一个元素开始,通过回溯 lisLengths 数组,构造最长递增子序列。

代码示例:

public class LongestIncreasingSubsequence {
    public static void main(String[] args) {
        int[] sequence = {10, 22, 9, 33, 21, 50, 41, 60, 80};
        int[] lisLengths = new int[sequence.length]; // 用于存储以每个位置为结尾的最长递增子序列长度
        
        Arrays.fill(lisLengths, 1); // 初始化lisLengths数组,每个元素本身就是一个递增子序列的长度
        
        int maxLength = 1; // 最长递增子序列的长度
        int lastIndex = 0; // 最长递增子序列的最后一个元素的位置
        
        // 动态规划计算最长递增子序列
        for (int i = 1; i < sequence.length; i++) {
            for (int j = 0; j < i; j++) {
                if (sequence[j] < sequence[i]) {
                    lisLengths[i] = Math.max(lisLengths[i], lisLengths[j] + 1);
                }
            }
            if (lisLengths[i] > maxLength) {
                maxLength = lisLengths[i]; // 更新最长递增子序列长度
                lastIndex = i; // 记录最长递增子序列的最后一个元素的位置
            }
        }
        
        List<Integer> longestIncreasingSubsequence = new ArrayList<>();
        longestIncreasingSubsequence.add(sequence[lastIndex]); // 将最长递增子序列的最后一个元素添加到列表中
        
        // 回溯构造最长递增子序列
        for (int i = lastIndex - 1; i >= 0; i--) {
            if (sequence[i] < sequence[lastIndex] && lisLengths[i] == lisLengths[lastIndex] - 1) {
                longestIncreasingSubsequence.add(sequence[i]);
                lastIndex = i;
            }
        }
        Collections.reverse(longestIncreasingSubsequence); // 反转列表,得到正确的顺序
        
        // 输出结果
        System.out.println("最长递增子序列长度:" + maxLength);
        System.out.println("最长递增子序列:" + longestIncreasingSubsequence);
    }
}

运行结果:

最长递增子序列长度:6
最长递增子序列:[10, 22, 33, 50, 60, 80]

4、最大子数组和问题。

解题思路:

  1. 创建一个与原数组长度相同的数组 maxSum,用于存储以每个位置为结尾的最大子数组和。
  2. 初始化 maxSum 数组的第一个元素为原数组的第一个元素。
  3. 遍历原数组,对于每个位置,计算以当前位置为结尾的最大子数组和,这可以通过比较当前位置的元素值和前一个位置的最大子数组和加上当前位置的元素值。
  4. 记录全局的最大子数组和。

代码示例:

public class MaxSubarraySum {
    public static void main(String[] args) {
        int[] nums = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
        
        int maxSum = nums[0]; // 最大子数组和
        int currentSum = nums[0]; // 当前位置为结尾的最大子数组和
        
        // 动态规划计算最大子数组和
        for (int i = 1; i < nums.length; i++) {
            currentSum = Math.max(nums[i], currentSum + nums[i]);
            maxSum = Math.max(maxSum, currentSum);
        }
        
        // 输出结果
        System.out.println("最大子数组和:" + maxSum);
    }
}

运行结果:

最大子数组和:6

六、动态规划

1、八皇后问题。

解题思路:
八皇后问题可以通过回溯法来解决。基本思路是从第一行开始,逐行放置皇后,每次在当前行找一个合适的列来放置皇后,然后递归到下一行。如果在某一行找不到合适的列,就回溯到上一行重新选择列。当放置完所有皇后后,得到一个合法解。
代码实现:

public class EightQueensProblem {
    private static final int N = 8; // 棋盘大小
    private static int[] queens = new int[N]; // 存储每行皇后所在的列

    public static void main(String[] args) {
        solve(0); // 从第一行开始解决问题
    }

    // 检查是否可以在第row行的col列放置皇后
    private static boolean isValid(int row, int col) {
        for (int i = 0; i < row; i++) {
            if (queens[i] == col || Math.abs(row - i) == Math.abs(col - queens[i])) {
                return false;
            }
        }
        return true;
    }

    // 递归解决八皇后问题
    private static void solve(int row) {
        if (row == N) { // 所有皇后已放置完毕
            printQueens();
            return;
        }
        for (int col = 0; col < N; col++) {
            if (isValid(row, col)) {
                queens[row] = col; // 在当前行的col列放置皇后
                solve(row + 1); // 递归到下一行
            }
        }
    }

    // 打印皇后的摆放情况
    private static void printQueens() {
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                if (queens[i] == j) {
                    System.out.print("Q ");
                } else {
                    System.out.print(". ");
                }
            }
            System.out.println();
        }
        System.out.println();
    }
}
. Q . . . . . .
. . . . Q . . .
. . . . . . . Q
. . . . . Q . .
Q . . . . . . .
. . Q . . . . .
. . . . . . Q .
. . . Q . . . .

2、数独问题。

解题思路:
数独问题是在一个9x9的网格中填入数字,要求每行、每列和每个3x3的子网格中都含有数字1到9,且不能重复。解题思路是使用回溯算法,从左上角开始,逐个位置尝试填入数字,然后递归到下一个位置。如果当前位置不能填入合适的数字,就回溯到上一个位置重新尝试。通过逐步试错,可以找到合适的数字组合,填满整个数独盘面。

代码实现:

以下是一个解数独问题的示例 Java 代码,包含详细注释:

public class SudokuSolver {
    private static final int N = 9;
    
    public static void main(String[] args) {
        int[][] board = {
            {5, 3, 0, 0, 7, 0, 0, 0, 0},
            {6, 0, 0, 1, 9, 5, 0, 0, 0},
            {0, 9, 8, 0, 0, 0, 0, 6, 0},
            {8, 0, 0, 0, 6, 0, 0, 0, 3},
            {4, 0, 0, 8, 0, 3, 0, 0, 1},
            {7, 0, 0, 0, 2, 0, 0, 0, 6},
            {0, 6, 0, 0, 0, 0, 2, 8, 0},
            {0, 0, 0, 4, 1, 9, 0, 0, 5},
            {0, 0, 0, 0, 8, 0, 0, 7, 9}
        };
        
        if (solveSudoku(board)) {
            printSudoku(board);
        } else {
            System.out.println("No solution exists.");
        }
    }
    
    private static boolean solveSudoku(int[][] board) {
        for (int row = 0; row < N; row++) {
            for (int col = 0; col < N; col++) {
                if (board[row][col] == 0) {
                    for (int num = 1; num <= N; num++) {
                        if (isValid(board, row, col, num)) {
                            board[row][col] = num;
                            if (solveSudoku(board)) {
                                return true;
                            }
                            board[row][col] = 0; // Backtrack
                        }
                    }
                    return false; // No valid number found
                }
            }
        }
        return true; // All cells filled
    }
    
    private static boolean isValid(int[][] board, int row, int col, int num) {
        for (int i = 0; i < N; i++) {
            if (board[row][i] == num || board[i][col] == num ||
                board[row - row % 3 + i / 3][col - col % 3 + i % 3] == num) {
                return false;
            }
        }
        return true;
    }
    
    private static void printSudoku(int[][] board) {
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                System.out.print(board[i][j] + " ");
            }
            System.out.println();
        }
    }
}

运行结果:
运行以上代码,在 IDEA 或其他 Java 开发环境中,将得到数独问题的一个解,示例如下:

5 3 4 6 7 8 9 1 2 
6 7 2 1 9 5 3 4 8 
1 9 8 3 4 2 5 6 7 
8 5 9 7 6 1 4 2 3 
4 2 6 8 5 3 7 9 1 
7 1 3 9 2 4 8 5 6 
9 6 1 5 3 7 2 8 4 
2 8 7 4 1 9 6 3 5 
3 4 5 2 8 6 1 7 9 

七、动态规划

1、零钱兑换问题。

解题思路:
创建一个数组 dp,其中 dp[i] 表示凑成金额 i 所需的最少硬币数量。
初始化 dp 数组,将其所有元素设置为一个特殊值,例如 Integer.MAX_VALUE - 1,表示无穷大,除了 dp[0] 设置为 0,因为凑成金额0不需要硬币。
对于每个硬币面额 coin,遍历金额 i 从 coin 到目标金额 amount,更新 dp[i] 为 dp[i - coin] + 1,即使用一枚硬币 coin,并查看是否可以凑成金额 i 的最优解。
最后,dp[amount] 就是凑成目标金额 amount 所需的最少硬币数量。

Java 代码示例:

public class CoinChange {
    public int coinChange(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        for (int i = 1; i <= amount; i++) {
            dp[i] = Integer.MAX_VALUE; // 初始化为正无穷大
        }

        for (int coin : coins) {
            for (int i = coin; i <= amount; i++) {
                if (dp[i - coin] != Integer.MAX_VALUE) {
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }

        return dp[amount] == Integer.MAX_VALUE ? -1 : dp[amount];
    }

    public static void main(String[] args) {
        CoinChange coinChange = new CoinChange();
        int[] coins = {1, 2, 5};
        int amount = 11;
        int minCoins = coinChange.coinChange(coins, amount);
        System.out.println("最少硬币数量:" + minCoins);
    }
}

运行结果:

对于输入 coins = {1, 2, 5} 和 amount = 11,运行上述代码会输出:

最少硬币数量:3

2、区间调度问题。

解题思路:
首先,将所有区间按照结束时间进行排序,以便在贪心选择时优先选择结束时间早的区间。
初始化一个变量 count 用于记录选择的不重叠区间数量,开始时设置为 0。
遍历排序后的区间列表,对于每个区间,如果它的开始时间大于或等于上一个选择的区间的结束时间,就将该区间加入选择列表,并将 count 增加 1。
最后,count 就是选择的最大不重叠区间数量。

Java 代码示例:

public class IntervalScheduling {
    public int maxNonOverlapping(int[][] intervals) {
        // 自定义排序函数,按结束时间升序排列(使用冒泡排序)
        int n = intervals.length;
        for (int i = 0; i < n - 1; i++) {
            for (int j = 0; j < n - i - 1; j++) {
                if (intervals[j][1] > intervals[j + 1][1]) {
                    int[] temp = intervals[j];
                    intervals[j] = intervals[j + 1];
                    intervals[j + 1] = temp;
                }
            }
        }

        int count = 0;
        int end = Integer.MIN_VALUE;

        for (int[] interval : intervals) {
            if (interval[0] > end) {
                // 如果当前区间的开始时间大于上一个选择的区间的结束时间,选择该区间
                count++;
                end = interval[1];
            }
        }

        return count;
    }

    public static void main(String[] args) {
        IntervalScheduling intervalScheduling = new IntervalScheduling();
        int[][] intervals = {{1, 2}, {2, 3}, {3, 4}, {1, 3}};
        int maxNonOverlapping = intervalScheduling.maxNonOverlapping(intervals);
        System.out.println("最大不重叠区间数量:" + maxNonOverlapping);
    }
}

运行结果:

对于输入 intervals 为 {{1, 2}, {2, 3}, {3, 4}, {1, 3}},运行上述代码会输出:

最大不重叠区间数量:2

八、位运算

1、求一个整数的二进制中 1 的个数。

解题思路:

使用位运算来逐位检查整数的二进制表示。
通过右移操作将整数的每一位逐一检查,检查最低位是否为1。
如果最低位是1,就将计数器加1。
右移整数,继续检查下一位,直到整数为0为止。

Java 代码示例:

public class CountOnesInBinary {
    public int countOnes(int n) {
        int count = 0;
        while (n != 0) {
            // 检查最低位是否为1
            if ((n & 1) == 1) {
                count++;
            }
            // 右移整数,继续检查下一位
            n = n >>> 1; // 使用无符号右移,防止负数陷入死循环
        }
        return count;
    }

    public static void main(String[] args) {
        CountOnesInBinary countOnesInBinary = new CountOnesInBinary();
        int num = 23;
        int onesCount = countOnesInBinary.countOnes(num);
        System.out.println(num + " 的二进制表示中1的个数为:" + onesCount);
    }
}

运行结果:

对于输入整数 num = 23,运行上述代码会输出:

23 的二进制表示中1的个数为:4

2、不使用额外空间交换两个整数的值。

要在不使用额外空间的情况下交换两个整数的值,可以使用位运算中的异或(XOR)操作。异或操作具有以下性质:

a ^ a = 0,对于任何整数a,自己与自己异或等于0。
a ^ 0 = a,对于任何整数a,与0异或等于它本身。
异或操作是可交换的,即a ^ b = b ^ a。

基于这些性质,我们可以实现不使用额外空间来交换两个整数的值。下面是示例代码:

public class SwapIntegersWithoutExtraSpace {
    public static void main(String[] args) {
        int a = 5;
        int b = 7;

        System.out.println("交换前:");
        System.out.println("a = " + a);
        System.out.println("b = " + b);

        // 使用异或操作交换a和b的值
        a = a ^ b;
        b = a ^ b;
        a = a ^ b;

        System.out.println("交换后:");
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
}

运行结果:
对于输入的 a = 5 和 b = 7,运行上述代码会输出:

交换前:
a = 5
b = 7
交换后:
a = 7
b = 5

通过三次异或操作,我们成功地交换了a和b的值,而不使用额外的空间。

九、其他问题:

1、最长公共子串。

解题思路:

使用两个嵌套的循环来比较两个字符串的每个字符。
对于每一对字符,如果它们相等,就将一个计数器加1。
如果字符不相等,就重置计数器为0。
在每次比较后,保持记录最大计数器值。
最终,最大计数器值即为最长公共子串的长度。

Java 代码示例:

public class LongestCommonSubstring {
    public static String findLongestCommonSubstring(String str1, String str2) {
        int maxLength = 0; // 最长公共子串的长度
        int endIndex = 0; // 最长公共子串的结束索引
        int[][] dp = new int[str1.length() + 1][str2.length() + 1]; // 用于存储公共子串长度信息

        for (int i = 1; i <= str1.length(); i++) {
            for (int j = 1; j <= str2.length(); j++) {
                if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1; // 如果字符相等,增加公共子串长度
                    if (dp[i][j] > maxLength) {
                        maxLength = dp[i][j];
                        endIndex = i - 1; // 更新最长公共子串的结束索引
                    }
                }
            }
        }

        if (maxLength == 0) {
            return ""; // 没有找到公共子串
        }

        // 截取最长公共子串
        int startIndex = endIndex - maxLength + 1;
        return str1.substring(startIndex, endIndex + 1);
    }

    public static void main(String[] args) {
        String str1 = "ABABC";
        String str2 = "BABCAB";
        String longestCommonSubstring = findLongestCommonSubstring(str1, str2);
        System.out.println("最长公共子串:" + longestCommonSubstring);
    }
}

运行结果:

对于输入的 str1 = “ABABC” 和 str2 = “BABCAB”,运行上述代码会输出:

最长公共子串:BABC

2、最长公共子序列。

最长公共子序列(Longest Common Subsequence,简称LCS)是一个经典的字符串处理问题,它的目标是找到两个字符串中最长的公共子序列,这个子序列可以不必是连续的。以下是解决这个问题的解题思路、Java示例代码以及运行结果。

解题思路:

使用动态规划来解决LCS问题。
创建一个二维数组 dp,其中 dp[i][j] 表示字符串 str1 的前 i 个字符和字符串 str2 的前 j 个字符的LCS长度。
初始化 dp 数组的第一行和第一列为0,因为任何一个字符串和空字符串的LCS都是0。
遍历 dp 数组,如果 str1[i - 1] 等于 str2[j - 1],则 dp[i][j] 可以从 dp[i - 1][j - 1] 增加1。
否则,dp[i][j] 是 dp[i - 1][j] 和 dp[i][j - 1] 中的较大值。
最终 dp[str1.length()][str2.length()] 就是最长公共子序列的长度。

public class LongestCommonSubsequence {
    public static String findLongestCommonSubsequence(String str1, String str2) {
        int[][] dp = new int[str1.length() + 1][str2.length() + 1];

        // 填充dp数组
        for (int i = 1; i <= str1.length(); i++) {
            for (int j = 1; j <= str2.length(); j++) {
                if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        // 构建最长公共子序列
        StringBuilder lcs = new StringBuilder();
        int i = str1.length();
        int j = str2.length();
        while (i > 0 && j > 0) {
            if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
                lcs.insert(0, str1.charAt(i - 1));
                i--;
                j--;
            } else if (dp[i - 1][j] > dp[i][j - 1]) {
                i--;
            } else {
                j--;
            }
        }

        return lcs.toString();
    }

    public static void main(String[] args) {
        String str1 = "ABCD";
        String str2 = "ACDF";
        String longestCommonSubsequence = findLongestCommonSubsequence(str1, str2);
        System.out.println("最长公共子序列:" + longestCommonSubsequence);
    }
}

运行结果:

对于输入的 str1 = “ABCD” 和 str2 = “ACDF”,运行上述代码会输出:

最长公共子序列:ACD

3、最小生成树问题。

最小生成树问题是图论中的经典问题,目标是在一个带权重的无向图中找到一个包含所有顶点的生成树,使得树的所有边的权重之和最小。通常采用Kruskal算法或Prim算法来解决这个问题,下面我将提供Kruskal算法的解题思路、Java示例代码以及运行结果。

解题思路(Kruskal算法):

将所有的边按照权重升序排列。
初始化一个空的最小生成树。
从排好序的边中逐个选择边,将它们加入最小生成树,但要确保不形成环路。
当最小生成树包含了所有顶点时,停止。
Kruskal算法是一种贪心算法,通过按权重升序选择边,保证了每次选择的边都是当前最小的。

Java 代码示例:

以下是使用Java语言实现Kruskal算法的示例代码。请注意,为了简化示例,我们使用邻接矩阵表示图,并假设图是连通的。

public class MinimumSpanningTree {
    static class Edge implements Comparable<Edge> {
        int src, dest, weight;

        public Edge(int src, int dest, int weight) {
            this.src = src;
            this.dest = dest;
            this.weight = weight;
        }

        @Override
        public int compareTo(Edge other) {
            return this.weight - other.weight;
        }
    }

    private static void kruskalMST(int[][] graph) {
        int V = graph.length;

        Edge[] edges = new Edge[V * V];
        int edgeCount = 0;

        // 构建边集合
        for (int i = 0; i < V; i++) {
            for (int j = i + 1; j < V; j++) {
                if (graph[i][j] != 0) {
                    edges[edgeCount++] = new Edge(i, j, graph[i][j]);
                }
            }
        }

        // 按权重升序排序边
        Arrays.sort(edges, 0, edgeCount);

        int[] parent = new int[V];
        Arrays.fill(parent, -1);

        System.out.println("最小生成树的边:");

        for (int i = 0; i < edgeCount; i++) {
            Edge edge = edges[i];
            int x = find(parent, edge.src);
            int y = find(parent, edge.dest);

            if (x != y) {
                System.out.println(edge.src + " - " + edge.dest + " 权重:" + edge.weight);
                union(parent, x, y);
            }
        }
    }

    private static int find(int[] parent, int vertex) {
        if (parent[vertex] == -1) {
            return vertex;
        }
        return find(parent, parent[vertex]);
    }

    private static void union(int[] parent, int x, int y) {
        int xRoot = find(parent, x);
        int yRoot = find(parent, y);
        parent[xRoot] = yRoot;
    }

    public static void main(String[] args) {
        int[][] graph = {
            {0, 2, 0, 6, 0},
            {2, 0, 3, 8, 5},
            {0, 3, 0, 0, 7},
            {6, 8, 0, 0, 9},
            {0, 5, 7, 9, 0}
        };

        kruskalMST(graph);
    }
}

运行结果:

对于上述示例代码中的图,运行后会输出最小生成树的边:

最小生成树的边:
0 - 1 权重:2
1 - 2 权重:3
0 - 3 权重:6
1 - 4 权重:5

4、图的拓扑排序。

拓扑排序是一种对有向图进行排序的算法,其排序结果满足图中所有边的方向。拓扑排序通常用于描述一组任务或依赖关系,确保任务按照依赖关系的顺序执行。以下是解决拓扑排序问题的解题思路、Java示例代码以及运行结果。

解题思路:

创建一个队列(可以使用数组模拟)和一个数组来存储每个顶点的入度(即指向该顶点的边的数量)。
初始化队列,将所有入度为0的顶点加入队列。
循环执行以下步骤,直到队列为空:
从队列中取出一个顶点,输出该顶点。
对该顶点的所有邻接顶点,减少其入度。
如果邻接顶点的入度变为0,将其加入队列。
如果输出的顶点数量等于图中的顶点数量,则拓扑排序完成;否则,表示图中存在环路,无法进行拓扑排序。
Java 代码示例:

以下是一个使用Java语言实现拓扑排序的示例代码,不依赖任何Java包:

public class TopologicalSort {
    private int V; // 顶点的数量
    private List<Integer>[] adjacencyList;

    public TopologicalSort(int V) {
        this.V = V;
        adjacencyList = new ArrayList[V];
        for (int i = 0; i < V; i++) {
            adjacencyList[i] = new ArrayList<>();
        }
    }

    public void addEdge(int u, int v) {
        adjacencyList[u].add(v);
    }

    public List<Integer> topologicalSort() {
        List<Integer> result = new ArrayList<>();
        int[] inDegree = new int[V];

        // 计算每个顶点的入度
        for (int u = 0; u < V; u++) {
            for (int v : adjacencyList[u]) {
                inDegree[v]++;
            }
        }

        Queue<Integer> queue = new LinkedList<>();

        // 将入度为0的顶点加入队列
        for (int u = 0; u < V; u++) {
            if (inDegree[u] == 0) {
                queue.offer(u);
            }
        }

        while (!queue.isEmpty()) {
            int u = queue.poll();
            result.add(u);

            // 更新邻接顶点的入度
            for (int v : adjacencyList[u]) {
                inDegree[v]--;
                if (inDegree[v] == 0) {
                    queue.offer(v);
                }
            }
        }

        // 如果结果中的顶点数量不等于图的顶点数量,表示图中存在环路,无法拓扑排序
        if (result.size() != V) {
            System.out.println("图中存在环路,无法进行拓扑排序。");
            return new ArrayList<>();
        }

        return result;
    }

    public static void main(String[] args) {
        TopologicalSort graph = new TopologicalSort(6);
        graph.addEdge(5, 2);
        graph.addEdge(5, 0);
        graph.addEdge(4, 0);
        graph.addEdge(4, 1);
        graph.addEdge(2, 3);
        graph.addEdge(3, 1);

        List<Integer> result = graph.topologicalSort();
        if (!result.isEmpty()) {
            System.out.println("拓扑排序结果:" + result);
        }
    }
}

运行结果:

对于上述示例代码中的有向图,运行后会输出拓扑排序的结果:

拓扑排序结果:[4, 5, 2, 0, 3, 1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Key-Key

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值