LeetCode 08 - 分治

23. 合并K个升序链表#

方法:分治合并

每次两两合并,直到合并成两个有序链表,最后将这两个链表合并即可。

ListNode mergeKLists(ListNode[] lists) {
    return merge(lists, 0, lists.length - 1);
}

ListNode merge(ListNode[] lists, int left, int right) {
    if(left > right) return null;
    if(left == right) return lists[left];
    int mid = (left + right) / 2;
    ListNode mergedLeft = merge(lists, left, mid);
    ListNode mergedRight = merge(lists, mid+1, right);
    return merge2Lists(mergedLeft, mergedRight);
}

ListNode merge2Lists(ListNode a, ListNode b) {
    if(a == null || b == null)
        return a == null ? b : a;
    ListNode dummyHead = new ListNode(-1);
    ListNode tail = dummyHead, pa = a, pb = b;
    while(pa != null && pb != null) {
        if(pa.val < pb.val) {
            tail.next = pa;
            pa = pa.next;
        } else {
            tail.next = pb;
            pb = pb.next;
        }
        tail = tail.next;
    }
    tail.next = pa == null ? pb : pa;
    return dummyHead.next;
}

169. 多数元素#

给定一个大小为 n 的数组,找到其中的多数元素。多数元素是指在数组中出现数 大于 ⌊ n/2 ⌋ 的元素。

方法一:哈希表

借助 HashMap 记录每个数字出现的次数,再遍历哈希表,找出值大于 n/2 的键。

方法二:排序

对数组排序(递增递减皆可)Arrays.sort(nums),那么下标在 n/2 的元素一定是答案。

方法三:随机化

因为多数元素占据了一半以上,所以可以随机挑选一个元素,检查它是不是众数,这样有很大概率找到答案。

int countOccurences(int[] nums, int num) {
    int count = 0;
    for(int i = 0; i < nums.length; i++)
        if(nums[i] == num) count++;
    return count;
}

int majorityElement(int[] nums) {
    int len = nums.length;
    Random rand = new Random();
    int majorityCount = len/2;
    while(true) {
        int randNum = rand.nextInt(len);
        if(countOccurences(nums, randNum) > majorityCount)
            return randNum;
    }
}

方法四:分治

如果 a 是数组 nums 的众数,那么将数组分成两个子数组,a仍然是其中至少一个子数组的众数。

这样一来就可以用分治法了:将数组分成左右两部分,分别求出左半部分的众数 a1 以及右半部分众数 a2 ,然后在这两个众数中选出答案。

int countInRange(int[] nums, int target, int low, int high) {
    int count = 0;
    for(int i = low; i <= high; i++)
        if(nums[i] == target) count++;
    return count;
}

int recurse(int[] nums, int low, int high) {
    // base case
    if(low == high) return nums[low];
    // 递归
    int mid = (low + high) / 2;
    int left = recurse(nums, low, mid);
    int right = recurse(nums, mid + 1, high);
    // 如果两个众数相同直接返回
    if(left == right) return left;
    // 不相同则进一步比较
    int leftCount = countInRange(nums, left, low, high);
    int rightCount = countInRange(nums, right, low, high);
    return leftCount > rightCount ? left : right;
}

int majorElement(int nums) {
    return recurse(nums, 0, nums.length - 1);
}

方法五:Boyer-Moore 投票算法

基本思想是,把 majority 元素记为 +1,把其他元素记为 -1,则所有元素的和会大于 0(因为题目保证一定存在 majority 元素)。具体算法过程为:

  • 维护一个候选元素 candidate 及其出现次数 count ,初始时候选元素任选,次数为 0。
  • 遍历数组,在处理每个元素 x 之前,如果 count==0 ,则将 x 作为 candidate ,这时候,
    • 如果 x 和原来的 candidate 相同,则 count++ (变成 1)。
    • 否则, count-- (变成 -1)。
  • 遍历结束后, candidate 即为整个数组的 majority 元素。

为什么这么做是对的呢?以 candidate 变成 0 的位置为界,前半部分数组中,majority 元素的个数 <= 其他数的总数,所以后半部分数组中,majority 元素的个数一定 > 其他所有数的总数,也就是说 majority 元素也是后半部分子数组的 majority 元素。总的来说, candidate=0 将数组分成若干段,每个 candidate=0 右边的子数组的 majority 元素一定是整个数组的 majority 元素。

int majority(int[] nums) {
    int candidate = -1, count = 0;
    for(int num : nums) {
        // 在分界点换人
        if(count == 0) candidate = num;
        if(num == candidate) count++;
        else count--;
    }
    return candidate;
}

240. 搜索二维矩阵 II#

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

方法一:二分搜索

在每一行中寻找目标值时,利用二分搜索。

boolean searchMatrix(int[][] matrix, int target) {
    for(int[] row : matrix) {
        if(search(row, target))
        return true;
    }
    return false;
}

boolean search(int[] nums, int target) {
    int left = 0, right = nums.length - 1;
    while(left <= right) {
        int mid = (left + right) / 2;
        int midValue = nums[mid];
        if(midValue == target)
            return true;
        else if(midValue < target)
            left = mid + 1;
        else
            right = mid - 1;
    }
    return false;
}

方法二:Z字形搜索

从矩阵右上角开始搜索,在每一步搜索过程中,如果当前位置为 (,)=(x,y),那么搜索范围确定为:以矩阵左下角为左下角,以当前位置为右上角的矩阵范围。

  • 如果 matrix[x][y] = target ,说明搜索完成;
  • 如果 matrix[x][y] > target ,说明当前列 y 中所有 x 行之下的元素都大于目标值,所以可以直接将 y--
  • 如果 matrix[x][y] < target ,说明当前行 x 中所有 y 列之前的元素都小于目标值,可以直接将 x++
  • 如果超出矩阵边界,说明不存在 target。
boolean searchMatrix(int[][] matrix, int target) {
    int rows = matrix.length, cols = matrix[0].length;
    int row = 0, col = cols-1;
    while(row < rows && col >= 0) {
        if(matrix[row][col] == target)
            return true;
        else if(matrix[row][col] < target)
            row++;
        else
            col--;
    }
    return false;
}

148. 排序链表#

将给定链表升序排序,并返回排序后的链表。

最适合链表的排序算法是归并排序,归并排序基于分治算法,最容易想到的实现方式是自顶向下的递归实现,但是递归调用的栈空间为 O(logn),所以要达到常数的空间复杂度,需要使用自底向上的方式。

方法一:自顶向下归并排序

过程如下:

  1. 找到链表的中点,将链表拆分为两个子链表;(快慢指针)
  2. 对两个子链表分别排序;
  3. 将排序后的子链表进行合并。
ListNode sortList(ListNode head) {
    return sortListHelper(head, null);
}

ListNode sortListHelper(ListNode head, ListNode tail) {
    // 递归终止条件:只剩一个/0个结点
    if(head == null) return null;
    if(head.next == tail) {
        head.next = null;
        return head;
    }
    // 找到中点
    ListNode slow = head, fast = head;
    while(fast != tail) {
        slow = slow.next;
        fast = fast.next;
        if(fast != tail)
            fast = fast.next;
    }
    ListNode mid = slow;
    // 递归排序两个子链表
    ListNode list1 = sortListHelper(head, mid);
    ListNode list2 = sortListHelper(mid, tail);
    // 将子链表合并成一个有序链表
    ListNode sorted = merge(list1, list2);
    return sorted;
}

// 合并两个链表,变成一个有序链表
public ListNode merge(ListNode head1, ListNode head2) {
    ListNode dummyHead = new ListNode(0);
    ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
    while(temp1 != null && temp2 != null) {
        if(temp1.val <= temp2.val) {
            temp.next = temp1;
            temp1 = temp1.next;
        } else {
            temp.next = temp2;
            temp2 = temp2.next;
        }
        temp = temp.next;
    }
    if(temp1 != null) temp.next = temp1;
    else if (temp2 != null) temp.next = temp2;
    return dummyHead.next;
}

方法二:自底向上归并排序

过程如下:

  1. subLength 表示每次需要排序的子链表的长度,初始时 subLength=1
  2. 每次将链表拆分成若干个长度为 subLength 的子链表,按照每两个子链表一组进行合并,合并后即可得到若干个长度为 subLength * 2 的有序子链表。
  3. subLength 加倍,重复第 2 步,对更长的有序子链表进行合并,直到有序子链表的长度达到 length
ListNode sortList(ListNode head) {
    if(head == null) return null;
    // 求出链表长度
    int length = 0;
    ListNode node = head;
    while(node != null) {
      length++;
      node = node.next;
    }
    ListNode dummyHead = new ListNode(0, head);
    // 每次将链表拆分为若干个长度为 subLength 的链表,并两两一组合并
    for(int subLength = 1; subLength < length; subLength <<= 1) {
        // cur 用于记录拆分位置, prev 用来串联合并后的子链表
        ListNode prev = dummyHead, cur = dummyHead.next;
        while(cur != null) {
            // 拆出来 subLength 长度的链表1
            ListNode head1 = cur;
            for(int i = 1; i < subLength && cur != null && cur.next != null; i++)
                cur = cur.next;
            // 拆出来 subLength 长度的链表2
            ListNode head2 = cur.next; // 链表2的头就是链表1尾结点的下一个节点
            cur.next = null; // 断开链表1和2的连接
            cur = head2;
            // 找到链表2的结束位置
            for(int i = 1; i < subLength && cur != null && cur.next != null; i++)
                cur = cur.next;
            ListNode next = null; // next 缓存下一个子链表的开始位置
            if(cur != null) {
                next = cur.next;
                cur.next = null; // 断开链表2和后面的连接
            }
            // 合并两个子链表的
            ListNode merged = merge(head1, head2);
            // 将合并后的子链表串联到总链表
            prev.next = merged;
            // 更新 prev 和 cur 指针
            while(prev.next != null)
                prev = prev.next;
            cur = next;
        }
    }
}

两个方法的时间都是:O(nlogn)

posted @   李志航  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示
主题色彩