数据结构与算法总结1(个人原创,带详细注释)

LEETCODE2

92. 反转链表 II

给你单链表的头指针 head 和两个整数 leftright ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表

示例 1:

输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

示例 2:

输入:head = [5], left = 1, right = 1
输出:[5]
链表双指针
class Solution {
public:
    ListNode* reverseBetween(ListNode* head, int left, int right) {
        int gap = right-left+1;
        //需要一个哨兵节点,防止从第一个节点开始就被反转
        ListNode* dummyhead = new ListNode(0,head);
        ListNode* p0 = dummyhead;
        while(left>1){//获取了需要反转的上一个节点待用
            left--;
            p0 = p0->next;
        }
        //接下来就是反转链表的三指针做法,需要注意的是一开始的Pre设置为nullptr,然后不断用nex记录后节点
        ListNode* pre = nullptr;
        ListNode* cur = p0->next;
        while(gap>0){
            gap--;
            ListNode* nex = cur->next;
            cur->next = pre;
            pre = cur;
            cur = nex;
        }
        //然后接一下头尾即可,最后结束时,cur为反转后面链的第一个节点,pre为反转链的最后一个节点
        p0->next->next = cur;
        p0->next = pre;
        return dummyhead->next;
    }
};
25. K 个一组翻转链表

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

链表双指针
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode* dummyhead = new ListNode(0,head);
        ListNode* p0 = dummyhead;
        int length = 0;
        while(head){
            head = head->next;
            length++;
        }
        head = dummyhead->next;
        length/=k;//算出有几组k个节点,也就是要做几组操作
        
        while(length>0){
            length--;
            ListNode* pre = nullptr;
            ListNode* cur = p0->next;//注意这里每次的cur都初始化为p0的下一个节点
            int gap = k;
            while(gap>0){
                ListNode* nex = cur->next;
                cur->next = pre;
                pre = cur;
                cur = nex;
                gap--;
            }
            ListNode* newp0 = p0->next;//先保存p0的下一个节点
            p0->next->next = cur;//反转头部
            p0->next = pre;//反转尾部
            p0 = newp0;//设置新的p0
        }
        return dummyhead->next;
    }
};
143. 重排链表

给定一个单链表 L 的头节点 head ,单链表 L 表示为:

L0 → L1 → … → Ln - 1 → Ln

请将其重新排列后变为:

L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …

不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例 1:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:head = [1,2,3,4]
输出:[1,4,2,3]

示例 2:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入:head = [1,2,3,4,5]
输出:[1,5,2,4,3]
链表

先找到链表中点,把中点到结尾部分的链表反转,得到两段链表

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

此时链表头部为head,链表尾部为head2,循环将head指向head2,head2指向head.next即可

注意循环的结束条件:head2->next!=nullptr

比如示例1,只循环了一次就退出了,结束时head = 2,head2 = 3,看起来始终没有更改节点2,3,但结果是正确的,结束时2->3->nullptr

示例2,结束时head = head2 = 3

class Solution {
public:
    ListNode* findmid(ListNode* head){
        ListNode *fast = head,*slow = head;
        while(fast&&fast->next){
            fast = fast->next->next;
            slow = slow->next;
        }
        return slow;
    }
    ListNode* reverselist(ListNode* head){
        ListNode *pre = nullptr;
        while(head){
            ListNode *nxt = head->next;
            head->next = pre;
            pre = head;
            head = nxt;
        }
        return pre;
    }
    void reorderList(ListNode* head) {
        ListNode *mid = findmid(head);//先找到中间节点
        ListNode *head2 = reverselist(mid);//反转链表
        while(head2->next){//注意循环终止条件
            ListNode *newhead1 = head->next,*newhead2 = head2->next;
            head->next = head2;
            head2->next = newhead1;
            head = newhead1,head2 = newhead2;
        }
    }
};
2171.拿出最少数目的魔法豆

给定一个 正整数 数组 beans ,其中每个整数表示一个袋子里装的魔法豆的数目。

请你从每个袋子中 拿出 一些豆子(也可以 不拿出),使得剩下的 非空 袋子中(即 至少还有一颗 魔法豆的袋子)魔法豆的数目 相等。一旦把魔法豆从袋子中取出,你不能再将它放到任何袋子中。

请返回你需要拿出魔法豆的 最少数目

示例 1:

输入:beans = [4,1,6,5]
输出:4
解释:
- 我们从有 1 个魔法豆的袋子中拿出 1 颗魔法豆。
  剩下袋子中魔法豆的数目为:[4,0,6,5]
- 然后我们从有 6 个魔法豆的袋子中拿出 2 个魔法豆。
  剩下袋子中魔法豆的数目为:[4,0,4,5]
- 然后我们从有 5 个魔法豆的袋子中拿出 1 个魔法豆。
  剩下袋子中魔法豆的数目为:[4,0,4,4]
总共拿出了 1 + 2 + 1 = 4 个魔法豆,剩下非空袋子中魔法豆的数目相等。
没有比取出 4 个魔法豆更少的方案。

题解

排序&反转思维

(求 取出的最少数目 没有思路,则将问题反转为求 剩下的最多数目

正难则反

我们可以将问题转化为:

寻找某一个数字 x x x,当我们将豆子数量小于 x x x 的袋子清空,并将豆子数量大于 x x x 的袋中豆子数量变为 x x x 时,拿出的豆子数量最少。

我们可以将 b e a n s beans beans 从小到大排序后,对 b e a n s beans beans遍历枚举最终非空袋子中魔法豆的数目 v v v,将小于 v v v 的魔法豆**(也就是在豆子数量为 v v v的袋子之前的全部袋子)** 全部清空,大于 v v v 的魔法豆减少至 v v v,这样所有非空袋子中的魔法豆就都相等了。

由于拿出魔法豆 + 剩余魔法豆 = 初始魔法豆之和,我们可以考虑最多能剩下多少个魔法豆,从而计算出最少能拿出多少个魔法豆。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如上图所示,可以保留蓝色矩形区域内的魔法豆。设 b e a n s beans beans 的长度为 n n n,以 n − i n−i ni 为矩形底边长, v = b e a n s [ i ] v=beans[i] v=beans[i]为矩形高,则矩形面积为

( n − i ) ∗ v (n−i)*v (ni)v

∑ b e a n s [ i ] ∑beans[i] beans[i] 减去矩形面积的最大值,即为拿出魔法豆的最小值。

class Solution {
public:
    long long minimumRemoval(vector<int>& beans) {
        long long total = 0,remain = 0;//注意这里beans总和和剩下的豆子总和都要是ll,不然会溢出
        sort(beans.begin(),beans.end());
        int size = beans.size();
        for(int i:beans) total+=i;
        for(int i=0;i<size;i++)
            remain = max(remain,(long long)beans[i]*(size-i));
        return total - remain;
    }
};
581. 最短无序连续子数组

给你一个整数数组 nums ,你需要找出一个 连续子数组 ,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序。

请你找出符合题意的 最短 子数组,并输出它的长度。

示例 1:

输入:nums = [2,6,4,8,10,9,15]
输出:5
解释:你只需要对 [6, 4, 8, 10, 9] 进行升序排序,那么整个表都会变为升序排序。

示例 2:

输入:nums = [1,2,3,4]
输出:0

示例 3:

输入:nums = [1]
输出:0
排序&双指针

先将数组拷贝一份进行排序,然后使用两个指针 i i i j j j 分别找到左右两端第一个不同的地方,那么 [ i , j ] [i,j] [i,j] 这一区间即是答案。

class Solution {
public:
    static bool cmp(int &n1, int &n2){
        return n1<n2;
    }
    int findUnsortedSubarray(vector<int>& nums) {
        if(nums.size()==1) return 0;
        vector<int> sorted(nums);
        sort(sorted.begin(),sorted.end(),cmp);
        int left = 0,right = nums.size()-1;
        while(left<nums.size()&&nums[left] == sorted[left]) left++;
        while(right>=0&&nums[right] == sorted[right]) right--;
        return max(0,right-left+1);
    }
};
贪心&单调栈

从左往右遍历,维护 单调递增栈,弹出的下标最小元素即为边界**(最左边的不满足小于右侧所有元素的元素)**

从右往左遍历,维护 单调递减栈,弹出的下标最大元素即为边界**(最右边的不满足大于左侧所有元素的元素)**

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        stack<int> stk;
        int left = INT_MAX,right = INT_MIN;
        for(int i=0;i<nums.size();i++){
            while(!stk.empty()&&nums[i]<nums[stk.top()]){
                left = min(left,stk.top());//取下标最小的不符合有序排列的元素(小于所有右侧元素)
                stk.pop();
            }
            stk.emplace(i);
        }
        while(!stk.empty()) stk.pop();
        for(int i=nums.size()-1;i>=0;i--){
            while(!stk.empty()&&nums[i]>nums[stk.top()]){
                right = max(right,stk.top());//取下标最大的不符合有序排列元素(大于所有左侧元素)
                stk.pop();
            }
            stk.emplace(i);
        }
        return right>left?right-left+1:0;
    }
};
贪心&双指针

如果最右端部分已经排好序,这部分的每个数都比它左边的最大值要大,同理,如果最左端部分排好序,这每个数都比它右边的最小值小。所以我们从左往右遍历,如果i位置上的数比它左边部分最大值小,则这个数肯定要排序, 就这样找到右端不用排序的部分,同理找到左端不用排序的部分,它们之间就是需要排序的部分

一言以蔽之:

「最后一个」比左边最大值小的元素为需要排序区间的右边界
「最后一个」比右边最小值大的元素为需要排序区间的左边界

从左往右 找 边界,因为需要维护左边已遍历的最小值,右边同理

class Solution {
public:
    int findUnsortedSubarray(vector<int>& nums) {
        if(nums.size()==1) return 0;
        int right = 0,left = nums.size()-1;
        int leftmax = INT_MIN,rightmin = INT_MAX;
        for(int i=0;i<nums.size();i++){
            right = nums[i]>=leftmax?right:i;
            leftmax = max(leftmax,nums[i]);
        }
        for(int i=nums.size()-1;i>=0;i--){
            left = nums[i]<=rightmin?left:i;
            rightmin = min(rightmin,nums[i]);
        }
        return max(right-left+1,0);
    }
};
670. 最大交换

给定一个非负整数,你至多可以交换一次数字中的任意两位。返回你能得到的最大值。

示例 1 :

输入: 2736
输出: 7236
解释: 交换数字2和数字7。

示例 2 :

输入: 9973
输出: 9973
解释: 不需要交换。

贪心的思路就是将最高位的但并不是当前位置上最大数字的数字与低位的最大数字交换位置

排序

记住逆序排序的两种写法

  1. 自定义比较函数 static bool cmp
  2. sort传入逆迭代器 rbegin rend

这种对int中个别数字操作的,用to_string和stoi转换成string操作

先由大到小排序,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),对比排序前后字符串,找出第一个不一样的字符,这个就是要交换的数,然后从没有排序的初始string开始,**从后往前(逆序遍历)**找出与排序后的数一样的数字,交换就完事了

  • 为什么是逆序遍历?如2887,有两个8,选哪个?如果选前面那个,则是8287,后面那个8827,因此从后往前找第一个数值等于排序后string相应位置的数
  • 因此对于相同的交换数,要选位置靠后的
class Solution {
public:
    static bool cmp(const char &c1,const char &c2){
        return c1>c2;
    }
    int maximumSwap(int num) {
        auto str = to_string(num);//原字符串
        auto sorted = str;//排序后字符串
        
        sort(sorted.rbegin(),sorted.rend());
        //sort(sorted.begin(),sorted.end(),cmp);
        
        int pos = 0;
        while(pos<str.length()&&str[pos]==sorted[pos]) pos++;//找排序前后字符串第一个不相等的数位置
        if(pos == str.length()) return num;
        
        int swappos = str.length()-1;
        while(str[swappos]!=sorted[pos]) swappos--;//从后往前逆序查找应该在排序后位置的数 
        
        swap(str[swappos],str[pos]);//原字符串中交换二者位置
        return stoi(str);
    }
};
倒序遍历 {#stack}

借鉴单调栈的倒序遍历思想,对每个元素枚举其右边的最大数,用一个数组存储

  • 倒序/从后往前 的精妙在于:所有右边的更大元素是已知的(因为已经被遍历了),单调栈也是如此思想
  • 1019.链表中下一个更大节点
  • 同时保证了相同值取位置靠后的,见上文

如果对于每个数字正序遍历找其更低位的最大数,那么相当于两重暴力循环,复杂度 O ( n 2 ) O(n^2) O(n2)

因为是更低位的最大数,所以从后往前遍历的同时,维护已遍历数的最大值,即可实现 O ( n ) O(n) O(n)

因此设一个数组对应原数组表示原数组下标元素后面的最大数,倒序遍历填这个数组,然后正序遍历原数组,找到第一个当前数不是最大数的元素,与最大数交换即可

class Solution {
public:
    int maximumSwap(int num) {
        auto str = to_string(num);
        vector<int> rightmax(str.length());//记录每个元素右侧最大值的 下标
        //int rtmax = str.back();
        int maxindex = str.length()-1;
        int i = str.length()-1;
        
        //倒序遍历,一边更新右侧最大值下标,一边对数组每个元素写入其右侧最大值下标
        while(i>=0){
            if(str[i]>str[maxindex])//这里只取大于号,保证了取位置靠后的相同值下标,相同的情况略过
                maxindex = i;
            rightmax[i] = maxindex;
            i--;
        }
        
        i++;
        while(i<str.length()&&str[rightmax[i]]==str[i]){
            i++;
        }
        if(i!=str.length())
            swap(str[i],str[rightmax[i]]);
        return stoi(str);
    }
};
滚动数组优化

注意到相对上面用数组存所有元素的右侧最大值,我们最后交换最靠左的,且当前值不是其右侧最大值的元素

因此只需要两个变量,一个记录最靠左的,且当前值不是其右侧最大值的元素下标,另一个倒序遍历时维护最大值下标

因为知道最靠左的,且当前值不是其右侧最大值的元素在何时出现,而最大值随遍历随时可能更新,有可能更新后的最大值在待交换元素前面,也就是在更新了最左侧的待交换元素后再次更新了最大值下标,这个时候,最终交换的最大值位置就错了

因此以上两个变量的维护是异步的

因此还需要一个下标,记录待交换元素更新时的右侧最大值

设 num 的十进制字符串为 s。算法如下:

倒序遍历 s,同时维护最大数的下标 maxIdx。它只在遇到更大的数字才更新,遇到相同数字不会更新,从而满足上面讨论的「最后一个」。

如果发现 s[i]<s[maxIdx],满足交换要求,我们先把这两个下标保存在变量 p 和 q 中。注:p 在遍历前的初始值为 −1。

继续向左遍历,如果又遇到 s[i]<s[maxIdx],就更新 p=i, q=maxIdx,因为 s[i] 越靠左越好,我们要交换的是从左到右第一个右边有比它大的数字

遍历结束,如果无需交换,即 p=−1,那么直接返回 num。否则交换 s[p] 和 s[q],然后把 s 转换成数字返回。

class Solution {
public:
    int maximumSwap(int num) {
        string s = to_string(num);
        int n = s.length();
        int max_idx = n - 1;
        int p = -1, q;
        for (int i = n - 2; i >= 0; i--) {
            //这里面两个if是异步的操作
            if (s[i] > s[max_idx]) { // s[i] 是目前最大数字,更新最大数字
                max_idx = i;
            } else if (s[i] < s[max_idx]) { // s[i] 右边有比它大的,更新交换位置
                p = i;
                q = max_idx; // 更新 p 和 q
            }
        }
        if (p == -1) { // 这意味着 s 是降序的
            return num;
        }
        swap(s[p], s[q]); // 交换 s[p] 和 s[q]
        return stoi(s);
    }
};
  • 24
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。
逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、图结构(有向图、无向图等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和图的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),图论算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值