【LeetCode热题100】打卡第34天:排序链表乘积最大的子数组

news2024/11/25 23:43:26

文章目录

  • 【LeetCode热题100】打卡第34天:排序链表&乘积最大的子数组
    • ⛅前言
  • 排序链表
    • 🔒题目
    • 🔑题解
  • 乘积最大的子数组
    • 🔒题目
    • 🔑题解

【LeetCode热题100】打卡第34天:排序链表&乘积最大的子数组

⛅前言

大家好,我是知识汲取者,欢迎来到我的LeetCode热题100刷题专栏!

精选 100 道力扣(LeetCode)上最热门的题目,适合初识算法与数据结构的新手和想要在短时间内高效提升的人,熟练掌握这 100 道题,你就已经具备了在代码世界通行的基本能力。在此专栏中,我们将会涵盖各种类型的算法题目,包括但不限于数组、链表、树、图、排序、搜索、动态规划等等,并会提供详细的解题思路以及Java代码实现。如果你也想刷题,不断提升自己,就请加入我们吧!QQ群号:827302436。我们共同监督打卡,一起学习,一起进步。

排序链表

🔒题目

原题链接:148.排序链表

image-20230710091853625

🔑题解

  • 解法一:暴力枚举(超时,30个示例,过了29个,一个超时)

    public class Solution {
        // Node.val的最小值
        private int MIN_VALUE = -100000;
        
        public ListNode sortList(ListNode head) {
            ListNode newHead = new ListNode(MIN_VALUE);
            while (head != null) {
                ListNode i = newHead;
                // 从 newHead 中定位比 head 大的前一个节点
                while (i.next != null && i.next.val < head.val) {
                    i = i.next;
                }
                ListNode tempNode = new ListNode(head.val);
                if (i.next != null){
                    // 非尾节点
                    tempNode.next = i.next;
                }
                i.next = tempNode;
                head = head.next;
            }
            return newHead.next;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为链表中元素的个数

  • 解法二:归并排序

    这一题,要求时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度为 O ( 1 ) O(1) O(1)基于这两点,很容易就联想到分治思想,而又结合排序就很容易想到归并排序,大家对于归并排序一定都熟记于心了吧(●ˇ∀ˇ●),虽然很容易想到,但是换成链表我就发现实现起来是比较困难的,这里先复习一下数组版的归并排序吧(采用自底向上的归并排序)

    public class Test {
        public static void main(String[] args) {
            int[] arr = {4, 2, 1, 1, 5, -1, 1, 7, 5};
            // 归并排序
            mergeSort(arr, 0, arr.length - 1);
            // 输出结果
            System.out.println(Arrays.toString(arr));
        }
    
        private static void mergeSort(int[] arr, int l, int r) {
            if (l == r) {
                // 区间中只有一个元素,无需划分
                return;
            }
            // 计算区间中间索引(向下取整,往左逼近,mid在左)
            int mid = (r - l) / 2 + l;
            // 划分左侧子数组
            mergeSort(arr, l, mid);
            // 划分右侧子数组
            mergeSort(arr, mid + 1, r);
            // 合并区间
            merge(arr, l, mid, r);
        }
    
        private static void merge(int[] arr, int l, int mid, int r) {
            int[] temp = new int[arr.length];
            int i = l;
            int j = mid + 1;
            int k = 0;
            // 比较左右子树组中的元素,将较小值放入temp中(降序排序)
            while (i <= mid && j <= r) {
                if (arr[i] < arr[j]) {
                    temp[k++] = arr[i++];
                } else {
                    temp[k++] = arr[j++];
                }
            }
            // 左侧子数组还有剩余
            while (i <= mid) {
                temp[k++] = arr[i++];
            }
            // 右侧子数组还有剩余
            while (j <= r) {
                temp[k++] = arr[j++];
            }
            // 将 temp 拷贝到 arr 中
            for (int t = 0; t < k; t++) {
                arr[l+t] = temp[t];
            }
        }
    }
    

    以下代码参考自 K神

    链表实现归并排序的难点在于如何确定中间节点?这里采用一个比较巧妙的技巧,那就是使用快慢指针,在前面(LeetCode热题100打卡33天)我们判断链表是否有换,也是用到了快慢指针,这里同样的可以利用它来确定中间节点

    大致思路:快指针(fast)比慢指针(slow)多走一步,这样fast到达了链表尾部,slow就处在了中间节点的位置,这里需要注意的是已经有左侧边界了,还缺一个右侧边界,如果直接以slow为右侧边界,会导致有节点遗漏,这个在二分查找中就已经讨论过了,这里不再赘述,所以我们需要以 slow.next 为右侧边界,此外在我们选中了 slow.next 为右侧边界,我们还需要将 slow.next置为null,这样能够比较好的用来判断左侧链表是否遍历到头,否则还需要使用一个多余的变量来记录左侧边界值

    image-20230710130741377

    class Solution {
        public ListNode sortList(ListNode head) {
            return mergeSort(head);
        }
    
        private ListNode mergeSort(ListNode node) {
            if (node == null || node.next == null) {
                return node;
            }
            // 定位中间节点(向上取整,往右逼近,mid在右)
            ListNode fast = node.next;
            ListNode slow = node;
            while (fast != null && fast.next != null) {
                slow = slow.next;
                fast = fast.next.next;
            }
            ListNode mid = slow.next;
            slow.next = null;
            // 划分左侧子区间
            ListNode left = mergeSort(node);
            // 划分右侧子区间
            ListNode right = mergeSort(mid);
            // 合并区间
            return merge(left, right);
        }
    
        private ListNode merge(ListNode left, ListNode right) {
            ListNode res = new ListNode();
            // 比较左右子区间中的元素,将较小值放入temp中(降序排序)
            ListNode temp = res;
            while (left != null && right != null) {
                if (left.val < right.val) {
                    temp.next = left;
                    left = left.next;
                } else {
                    temp.next = right;
                    right = right.next;
                }
                temp = temp.next;
            }
            // 将剩余的一方添加到 temp 的尾部
            temp.next = left != null ? left : right;
            return res.next;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为数组中元素的个数

    这种方法虽然能过,但是空间复杂度是 O ( n ) O(n) O(n),但这不符合进阶要求,进阶的要求是空间复杂度 O ( 1 ) O(1) O(1),所以递归实现归并是无法实现的,我们就需要使用迭代来实现归并排序:

    略……https://leetcode.cn/problems/sort-list/solution/javadi-gui-die-dai-shuang-zhong-jie-fa-luo-ji-qing/
    

    PS:迭代归并写起来好麻烦,不要折磨自己了,在我提交别人的代码测试后发现迭代归并虽然迭代归并空间复杂度为常数,但是内存消耗居然比递归归并还要多,耗时也要多,感兴趣的可以自行去LeetCode看别人迭代版的归并排序

  • 解法三:快速排序

    同理,先来复习以下数组快排是如何实现的吧😄

    public class Test {
        public static void main(String[] args) {
            int[] arr = {4, 2, 1, 1, 5, -1, 1, 7, 5};
            quickSort(arr, 0, arr.length - 1);
            System.out.println(Arrays.toString(arr));
        }
    
        private static void quickSort(int[] arr, int l, int r) {
            if (l >= r) {
                // 区间中只有一个元素时或无元素时,无需继续划分区间
                return;
            }
            // 划分区间,并获取主元索引
            int pivot = partition(arr, l, r);
            // 划分左侧子区间
            quickSort(arr, l, pivot - 1);
            // 划分右侧子区间
            quickSort(arr, pivot + 1, r);
        }
    
        private static int partition(int[] arr, int l, int r) {
            // 选取主元(以区间末尾元素为主元)
            int pivot = arr[r];
            // 左侧区间右边界
            int i = l - 1;
            // 划分区间(左侧区间<主元,右侧区间>=主元)
            for (int j = l; j < r; j++) {
                // 将比主元小的元素放到 i+1 的左侧
                if (arr[j] < pivot) {
                    swap(arr, ++i, j);
                }
            }
            // 将主元放到分界点,然后返回主元索引
            swap(arr, i + 1, r);
            return i + 1;
        }
    
        private static void swap(int[] arr, int i, int j) {
            int temp = arr[j];
            arr[j] = arr[i];
            arr[i] = temp;
        }
    }
    
    class Solution {
        public static ListNode sortList(ListNode head) {
            return quickSort(head, null);
        }
    
        public static ListNode quickSort(ListNode head, ListNode end) {
            if (head == end || head.next == end) {
                // 区间中只有一个元素时或无元素时,无需继续划分区间
                return head;
            }
            // 划分区间,并获取主元节点
            ListNode pivot = partition(head, end);
            // 划分左侧子区间
            ListNode node = quickSort(pivot, head);
            // 划分右侧子区间
            head.next = quickSort(head.next, end);
            return node;
        }
    
        private static ListNode partition(ListNode head, ListNode end) {
            // 选头节点的值作为主元
            int pivot = head.val;
            // 左区间右边界
            ListNode i = head;
            // 右区间右边界
            ListNode j = head;
            // 遍历整个区间,并进行划分(左侧区间<主元,右侧区间>=主元)
            ListNode p = head.next; // 从第二个节点开始遍历
            while (p != end) {
                // 临时存储 p.next,防止链表断裂
                ListNode tempNode = p.next;
                // 将所有比主元小的元素都放到 i 的左侧,所有比主元大的元素都放到 j 的右侧
                if (p.val < pivot) {
                    // 头插
                    p.next = i;
                    i = p;
                } else {
                    // 尾插
                    j.next = p;
                    j = p;
                }
                p = tempNode;
            }
            j.next = end;
            return i;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
    • 空间复杂度: O ( 1 ) O(1) O(1)

    其中 n n n 为数组中元素的个数

乘积最大的子数组

🔒题目

原题链接:152.乘积最大的子数组

image-20230710092107109

🔑题解

  • 解法一:暴力枚举(能过)

    class Solution {
        public int maxProduct(int[] nums) {
            int maxProduct = Integer.MIN_VALUE;
            for (int i = 0; i < nums.length; i++) {
                int product = 1;
                for (int j = i; j < nums.length; j++) {
                    product *= nums[j];
                    maxProduct = Math.max(maxProduct, product);
                }
            }
            return maxProduct;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n 2 ) O(n^2) O(n2)
    • 空间复杂度: O ( 1 ) O(1) O(1)
  • 解法二:动态规划

    解法一使用暴力枚举,每次都需要重新去计算左侧最大值,计算左侧最大值的过程其实是可以使用一个数组存储的,这样就不需要每次都去计算一遍左侧最大值,但是这样也带来了一个问题,那就是当前最大值,不一定是由左侧最大值更新过来的,有可能是由左侧最小值更新过来的,所以我们还需要使用一个数组去记录左侧最小值,总结起来,无非以下几种情况:

    max[i]:从第 0 个元素到 第 i 个元素(必须包含第i个元素),能够形成最大的连续乘积

    min[i]:从第 0 个元素到 第 i 个元素(必须包含第i个元素),能够形成最小的连续乘积

    1. 当前最大值由左侧最大值更新得到,max[i]=Math.max(nums[i],nums[i]*max[i-1])

    2. 当前最大值由左侧最小值更新得到,max[i]=Math.max(max[i],nums[i]*max[i-1])

      综合起来,就可以得到:max[i] = Math.max(max[i - 1] * nums[i], Math.max(nums[i], min[i - 1] * nums[i]))

      同理,我们可以得到:min[i] = Math.min(min[i - 1] * nums[i], Math.min(nums[i], max[i - 1] * nums[i]))

    最后我们只需要通过Math.max(ans, max[i])更新最大连续乘积即可

    class Solution {
        public int maxProduct(int[] nums) {
            int len = nums.length;
            int[] max = new int[len];
            max[0] = nums[0];
            int[] min = new int[len];
            min[0] = nums[0];
            // 更新计算出max数组和min数组
            for (int i = 1; i < len; ++i) {
                max[i] = Math.max(max[i - 1] * nums[i], Math.max(nums[i], min[i - 1] * nums[i]));
                min[i] = Math.min(min[i - 1] * nums[i], Math.min(nums[i], max[i - 1] * nums[i]));
            }
            // 更新计算出最大连续乘积
            int maxProduct = max[0];
            for (int i = 1; i < len; ++i) {
                maxProduct = Math.max(maxProduct, max[i]);
            }
            return maxProduct;
        }
    }
    

    我们可以发现在更新max和min数组时,其实也是可以顺便更新ans的,所以可以对上面代码进行简化

    class Solution {
        public int maxProduct(int[] nums) {
            int len = nums.length;
            int[] max = new int[len];
            max[0] = nums[0];
            int[] min = new int[len];
            min[0] = nums[0];
            int maxProduct = max[0];
            // 更新计算出max数组和min数组,同时更新ans
            for (int i = 1; i < len; ++i) {
                max[i] = Math.max(max[i - 1] * nums[i], Math.max(nums[i], min[i - 1] * nums[i]));
                min[i] = Math.min(min[i - 1] * nums[i], Math.min(nums[i], max[i - 1] * nums[i]));
                maxProduct = Math.max(maxProduct, max[i]);
            }
            return maxProduct;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ) O(n) O(n)
    • 空间复杂度: O ( n ) O(n) O(n)

    其中 n n n 为数组中元素的个数

    代码优化:空间优化

    我们发现每次更新,其实max和min后面的元素都没有用到,我们只用到了上一次的状态,这就我们的状态方程只有上一个状态有关,这就说明我们可以仅使用有关变量去记录上一个状态

    class Solution {
        public int maxProduct(int[] nums) {
            int len = nums.length;
            int max = nums[0];
            int min = nums[0];
            int maxProduct = nums[0];
            for (int i = 1; i < len; ++i) {
                int preMax = max;
                int preMin = min;
                max = Math.max(preMax * nums[i], Math.max(nums[i], preMin * nums[i]));
                min = Math.min(preMin * nums[i], Math.min(nums[i], preMax * nums[i]));
                maxProduct = Math.max(maxProduct, max);
            }
            return maxProduct;
        }
    }
    

    复杂度分析:

    • 时间复杂度: O ( n ) O(n) O(n)
    • 空间复杂度: O ( 1 ) O(1) O(1)

    其中 n n n 为数组中元素的个数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/740471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何将文字转化为语音?三个方法帮你轻松实现!

如何将文字转化为语音&#xff1f;在工作或学习中&#xff0c;我们可能会遇到需要将文字转化为语音的情况&#xff0c;这可能会让一些人感到困惑&#xff0c;不知道如何实现这个转换。其实&#xff0c;只需要利用一些第三方工具&#xff0c;就可以轻松地将文字转化为语音。下面…

十四、flex弹性容器属性样式2

目录&#xff1a; 1.准备工作 2.属性解析&#xff1a; align-items 3.属性解析&#xff1a; align-content 4.弹性元素的属性 一、准备工作 我们在前面的基础上&#xff0c;修改代码&#xff0c;把ul的高度定下来&#xff0c;设置800px, li的高度不定。 然后&#xff0c;body里…

音频采样器 Native Instruments Kontakt7 forMac/Windows图文安装教程

Native Instruments Kontakt是一款功能强大、灵活易用的音乐采样软件&#xff0c;适用于各种音乐创作和制作需求。无论是专业音乐制作人还是初学者&#xff0c;都能通过它来实现创意的音乐作品。 Kontakt具有直观的用户界面&#xff0c;可通过拖放方式导入和管理采样库。它支持…

Vmware虚拟机网络配置回顾

如何配置Vmware里的虚拟机网络&#xff1f;这个东西不常用&#xff0c;都是自己练手用的。能用就行&#xff0c;千万不要花时间记&#xff0c;没意义。 很简单&#xff0c;照着敲 首先登陆自己的虚拟机 vim /etc/sysconfig/network-scripts/ifcfg-ens32 TYPE"Ethernet&q…

数据结构--并查集

数据结构–并查集 逻辑结构―—“集合” 所有元素的全集s 将各个元素划分为若干个互不相交的子集 用互不相交的树&#xff0c;表示多个“集合” “并查集”的存储结构 用一个数组S[ ]即可表示“集合”关系 ‘并查集”的基本操作 集合的两个基本操作―— “并” \color{red}“…

ios14~14.3越狱/root(Taurine牛磺酸1.1.6)

Taurine牛磺酸 一键完美越狱 windows安装时建议关闭本地安全中心&#xff08;若报毒的话&#xff0c;没有则忽略&#xff09; 1.安装windows端AltInstaller&#xff1a;安装成功后&#xff0c;电脑右下角控制中心有一个&#xff08;灰色的 小方块&#xff09; 2.安装手机端A…

layui入门

layui入门 一.ayui简介1.简单易用2.组件丰富3.高度定制化4.响应式布局5.轻量灵活 2.layui的入门基础操作3.登录实例4.注册实例 一.ayui简介 Layui&#xff08;流行音 “layui”&#xff0c;来自“领域的模块化”&#xff09;是一款前端UI框架&#xff0c;专注于提升 Web 开发效…

Jmeter接口关联(三)【使用正则表达式提取值】以及正则表达式提取器中模板的含义及用法

文章目录 前言一、Jmeter中使用正则表达式匹配 1、选择 RegExp Tester2、在线程组------》添加------》后置处理器-------里面添加一个“正则表达式提取器”二、关于正则表达式提取器里面字段的解释 参数说明三、进一步解释Jmeter正则表达式提取器中的模板 1、当模板设置为$0$ …

每个开发人员都应该知道的VS Code入门技巧

这里有一些每个开发人员都应该知道的关于Visual Studio Code (VS Code)的技巧: 1、自定义键盘快捷键:VS Code允许您根据自己的喜好自定义键盘快捷键。点击“文件”->“首选项”->“键盘快捷键”或使用快捷键Ctrl K和Ctrl S打开键盘快捷键编辑器。可以修改现有快捷方式或…

抖音seo源码打包分享

抖音seo源码搭建----分享给各位开发者 获取视频列表 $Video_model new App_Model_Douyin_MysqlVideoStorage(); $video_list $Video_model->getList($where,$this->index,$this->count,$sort); $temp_video_model new App_Model_Douyin_…

微信小程序input的placeholder脱离文档流

今天进行真机调试时input的提示词 placeholder脱离了文档流&#xff0c;但是奇怪的是input框没有脱离文档流 如下图所示&#xff1a; 微信开发工具正常&#xff1a; 真机&#xff1a;不正常 脱离文档流 解决方法&#xff1a; <view clas…

给一个体积水,用不同体积的容器去装

这个有两个方案&#xff1a; 1.每个都装得最满&#xff0c;减少瓶子容积损失 //xzlist 瓶子容积排序 tj水总体积 xzzc各个体积瓶子数 public static void Boxjs(int tj, List<Map<String,Object>> xzlist, List<Map<String,Object>> xzzc){boolean f…

Linux信号机制-2

转自&#xff1a;Linux信号处理_linux 信号处理函数_努力啃C语言的小李的博客-CSDN博客 什么是信号 信号本质上是在软件层次上对中断机制的一种模拟&#xff0c;其主要有以下几种来源&#xff1a; 程序错误&#xff1a;除零&#xff0c;非法内存访问等。 外部信号&#xff1a…

Sql 语句小课堂8:求特定字段平均值的问题

Sql 语句小课堂8&#xff1a;求特定字段平均值的问题 问题来源初始数据超标条件方案一&#xff1a;得出汇总结果方案二&#xff1a;在原有数据上附加其结果 小结 问题来源 最近老顾变得原来越咸鱼了&#xff0c;好久没去逛 CSDN 问答了&#xff0c;于是灵感枯竭&#xff0c;不…

postgresql(二):pgsql导出数据

pgsql导出数据 1、概述2、导出数据2.1、导出所有库2.2、导出指定库2.3、导出指定表 3、总结 1、概述 大家好&#xff0c;我是欧阳方超&#xff0c;可以关注我的公众号“欧阳方超”&#xff0c;后续内容将在公众号首发。 今天介绍一下使用pg数据库的命令导出数据的操作。 2、导…

今天给大家分享几款好用的时间管理APP

在现代社会&#xff0c;时间是我们最宝贵的资源之一。有效地管理时间可以提高我们的工作和学习效率&#xff0c;从而实现更好的生活和工作质量。随着技术的不断发展&#xff0c;越来越多的时间管理APP涌现出来。今天&#xff0c;我想向大家分享几款好用的时间管理APP&#xff0…

没有有效的提示词(Prompt),要 Stable Diffusion 何用?

再好的prompt&#xff0c;不如有个简单的prompt工具。 本文推荐一个日本人写的prompt插件&#xff0c;我进行了汉化&#xff0c;并补充了3000多个提示词。只需要鼠标点点就可以了&#xff01;&#xff01;&#xff01; 引子&#xff1a; 今天去看了看Stable Diffusion插件版本…

MobPush Android常见问题

配置了默认点击跳转界面&#xff0c;对所有通道都有效吗 只对MobPush、魅族、小米、华为、OPPO、VIVO通道有效&#xff0c;对FCM通道无效。 如何获取回调参数 进程存活的情况下&#xff0c;可在我们的回调监听中看到通知详情&#xff0c;可以根据回调参数进行处理。 详情请查…

阿里云ECS云服务器的云盘使用

在我阿里云控制台上&#xff0c;可以看到有额外的磁盘&#xff08;2个实例&#xff0c;3个磁盘&#xff09; 找到对应云服务实例&#xff0c;看到了云盘信息 状态显示的挂接点是&#xff1a;/dev/xvdb 进入服务器却无法找到&#xff0c;也无法挂载 执行命令&#xff1a;fdisk …

c# 实现条件编译

创建三个不同的项目配置&#xff0c;分别代表不同的平台&#xff0c;在 Visual Studio 中&#xff0c;可以通过右键单击项目&#xff0c;选择“属性”&#xff0c;然后在“生成”选项卡下配置不同的条件编译符号。 针对 Windows 平台&#xff1a;在“生成”选项卡的“条件编译…