网课学习情况
1.编程类学习情况
上大学第一个月学完了黑马c++基础和浙大翁恺的c,等学完了侯捷的c++面向对象基础就去把黑马的两个项目补上
2.算法类学习情况
最终还是决定先看浙大的数据结构,目前学到树,但我觉得前面学习的递归(一看就懂一敲就废),和堆,队列,链表之类更需要巩固,所以现在决定一两周推一章的数据结构,余下的时间用来写leetcode
2.1 leetcode笔记梳理
2.1.1双指针
双指针的内容比较简单,毕竟本质上是一种思想
283. 移动零
给定一个数组
nums,编写一个函数将所有0移动到数组的末尾,同时保持非零元素的相对顺序。请注意 ,必须在不复制数组的情况下原地对数组进行操作。
这就是双指针的排列用途之一:让排列的数滚动前进,同时对他们进行特定动作(交换,判断插旗...) 代码比较简单,但官方写的更精巧,我直接搬运了
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int n = nums.size(), left = 0, right = 0;
while (right < n) {
if (nums[right]) {
swap(nums[left], nums[right]);
left++;
}
right++;
}
}
作用是让右指针不断滚动,一旦!=0就和左指针做交换,同时让左指针右移.妙就妙在左指针会始终指向0,因为若只有一个0就只会跳过一次swap,n次类推,从而0会一起跟着右指针向右滚动.
上面的主要是开拓视野,下面这道就比较朴实了
557. 反转字符串中的单词 III
给定一个字符串s,你需要反转字符串中每个单词的字符顺序,同时仍保留空格和单词的初始顺序。示例 1:
输入: s = "Let's take LeetCode contest" 输出: "s'teL ekat edoCteeL tsetnoc"示例 2:
输入: s = "God Ding" 输出: "doG gniD"
思路也很简单,就是循环for读s,直到读到' '或者读完,然后把这个单词的尾部丢到前面,快慢针碰撞就结束该交换.
当然,这也是双指针的别名,快慢针,本质也是交换.
class Solution {
public:
string reverseWords(string s)//快慢针
{
int i = 0;//前
int c = 0;//后
int cnt = 0;
for (auto a : s)
{
if (a == ' '||cnt==s.size()-1)
{
int slow = i;
int fast = c-1;
if(cnt==s.size()-1)
fast++;
for (; slow < fast; slow++, fast--)
{
swap(s[slow], s[fast]);
}
i = c+1;
}
c++;
cnt++;
}
return s;
}
};
2.1.2 二分查找
这个也是思想而非算法.并且十分简单易懂,套路也很固定,所以我只举一个例子(主要是懒)
主要用于用logn的时间找到你想要的数,不过这个和浙大上举的分支递归有异曲同工之妙,这个等等再提.
35. 搜索插入位置
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为
O(log n)的算法。示例 1:
输入: nums = [1,3,5,6], target = 5 输出: 2示例 2:
输入: nums = [1,3,5,6], target = 2 输出: 1示例 3:
输入: nums = [1,3,5,6], target = 7 输出: 4
class Solution
{
public:
int searchInsert(vector<int>& nums, int target)
{
int left = 0; int right = nums.size() - 1;
//int lastmid = 0;
int mid=0;
while (left <= right)
//*注意是<=,否则后面right要取mid而非mid-1.*
//*其实就是区间[a,b)和[a,b]的区别*
{
// lastmid = mid;
mid = (right - left)/2 + left;//r-l后/2再加是为了防止溢出
if (nums[mid] == target)
{
return mid;
}
else
{
nums[mid] > target ? right = mid - 1 : left = mid + 1;//r和l都不能==mid
}
}
if(nums[mid]>target)
return mid;
return mid + 1;
}
};
模板记住就好~
2.1.3分治递归(超纲..)
补上这一部分只是因为浙大讲了,分治递归的思想很棒,但我绝对想不出来...
int Max3( int A, int B, int C )
{ /* 返回3个整数中的最大值 */ return A > B ? A > C ? A : C : B > C ? B : C; }
int DivideAndConquer( int List[], int left, int right )
{ /* 分治法求List[left]到List[right]的最大子列和 */
int MaxLeftSum, MaxRightSum; /* 存放左右子问题的解 */
int MaxLeftBorderSum, MaxRightBorderSum; /*存放跨分界线的结果*/
int LeftBorderSum, RightBorderSum;
int center, i;
if( left == right )
{ /* 递归的终止条件,子列只有1个数字 */
if( List[left] > 0 ) return List[left];
else return 0;
} //递归出口
/* 下面是"分"的过程 */
center = ( left + right ) / 2; /* 找到中分点 */
/* 递归求得两边子列的最大和 */
MaxLeftSum = DivideAndConquer( List, left, center );
MaxRightSum = DivideAndConquer( List, center+1, right );
/* 下面求跨分界线的最大子列和 */
MaxLeftBorderSum = 0; LeftBorderSum = 0;
for( i=center; i>=left; i-- )
{ /* 从中线向左扫描 */
LeftBorderSum += List[i];
if( LeftBorderSum > MaxLeftBorderSum )
MaxLeftBorderSum = LeftBorderSum; } /* 左边扫描结束 */
MaxRightBorderSum = 0; RightBorderSum = 0;
for( i=center+1; i<=right; i++ )
{ /* 从中线向右扫描 */
RightBorderSum += List[i];
if( RightBorderSum > MaxRightBorderSum )
MaxRightBorderSum = RightBorderSum; } /* 右边扫描结束 */
/* 下面返回"治"的结果 */
return Max3( MaxLeftSum, MaxRightSum, MaxLeftBorderSum + MaxRightBorderSum ); }
//在递归中,这里既做了总对比的返回,也做了子问题的返回
int MaxSubseqSum3( int List[], int N )
{ /* 保持与前2种算法相同的函数接口 */
return DivideAndConquer( List, 0, N-1 ); }
其实递归的思想大概可以这么概括
首先,这是找查题 找查题最方便的(也是最"编程"的hh)就是遍历,一个个遍历.这不可能,于是我们想到二分查找,哎,我要找到不是具体的数,而是一串数哦,于是分治就出来了.
找数字起码要循环把,一个个循环找也好麻烦.
那就递归,循环能做到的,递归都可以.
1.找到子问题
找子问题可以从最小分配的形式入手,比如这道题的子问题就是左边,右边的总和怎么算?跨边界情况怎么判断?然后从最小分配的形式去尝试:最后的情况是每个数字的左右边界都是他自己.好,那倒数第二种情况呢?每个数字有左边一个,右边一个,中间的自己.这是不是很像我们要的子问题?左边,右边和中间.那判断条件就出来了.
2.找到出口
1.找到子问题中其实我们已经找到出口了,就是最小分配的情况.有的时候把递归想象成循环也可能可以找到出口.
当然递归我也还没开始学,这些也只是一些浅薄的推测,后续可能会更改.这里只是提一嘴,因为递归真的太妙了,虽然空间占用很恐怖,大多时候我也更喜欢迭代.
2.1.4滑动窗口
其实滑动窗口的核心思想也和双指针很像(不如说实现方法是一模一样hh)
3. 无重复字符的最长子串
给定一个字符串
s,请你找出其中不含有重复字符的 最长子串 的长度。
示例 1:
输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。示例 2:
输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。示例 3:
输入: s = "pwwkew" 输出: 3 解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列, 不是子串。
这个也是模板套就完事了
class Solution {
public:
int lengthOfLongestSubstring(string s)
{
int l = s.size();
int start = 0, end = 0, cnt = 0;
while (end < l)
{
for (int i = start; i < end; ++i)//不断从窗口左端与右端作对比
//!=则左端指针暂时右移
{
if (s[i] == s[end])
{
start = i+1;
break;
}
}
cnt = max(cnt,end-start+1);
end++; //右端始终循环++
}
return cnt;
}
};
值得纪念一下,这是我第一个自己做出来的时间击败100%hh
2.1.5链表
之前讨论的都是思想题,现在终于到容器/数据结构的专题了(因为都不熟hh),所以不像之前的章节,本章我也会尽量写出尽可能多的题解.
当然,章节较大所以我也只简单地写一部分题解和笔记.
23. 合并K个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入: lists = [[1,4,5],[1,3,4],[2,6]] 输出: [1,1,2,3,4,4,5,6] 解释: 链表数组如下: [ 1->4->5, 1->3->4, 2->6 ] 将它们合并到一个有序链表中得到。 1->1->2->3->4->4->5->6示例 2:
输入: lists = [] 输出: []示例 3:
输入: lists = [[]] 输出: []
1.暴力
拿到题的第一反应是暴力,后面分析了一下时间复杂度发现也不高(好像是o(n)?),果不其然也时间也打败了77%的人数.纪念一下第一次写困难题一次ac而且成绩不错hh.
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {
vector<int>v;
if (lists.empty())
return NULL;
for (int i = 0; i < lists.size(); ++i)//把所有链表中的数据保存下来
{
ListNode* p = lists[i];
while (p) {
v.push_back(p->val);
p = p->next;
}
}
sort(v.begin(), v.end());//升序排列数据
int l2 = v.size() - 1;
ListNode* Head=new ListNode();
ListNode* pend = Head;
for (int i = 0; i <= l2; ++i)//创建新的链表以链接
{
ListNode* t = new ListNode(v[i]);
pend->next = t;
pend = t;
}
return Head->next;
}
};
好,骄傲到此为止.
其实观察这题,不难发现其实本质是容器的排序,而且是数字排序,这就很难不让人想到分治法排列了,所以最大的问题成了"我如何用链表的形式进行分治"
2.分治法排序
再仔细观察这道题,可以发现链表是用vector的容器装载的,这就意味着他可以通过 [] 下标快速检索.然后再结合上面 2.1.3的分治递归.我们可以发现这道题的子问题就是"二分比较左右两边谁的链表的当前值大".而 出口就自然是链表指向NULL.
//和官方题解很像对吧,因为我是几天前看的题解,现在靠感觉+自己的理解写下来,没想到出奇的
//基本一致hh,不过可能递归分治基本都是这个套路,只是子问题不同,但查找的方法和beat的方法
//基本上一样的
//就像擂台赛一样,哪边一赢了就活下来,然后输的换下一个,直到这个队伍没人了.
//不过有的时候有记分员,有的时候没有,但淘汰赛的双方一定都会带着自己的成绩(有记分员时
//或者战利品(比如链表,就是对方和自己联合起来)进行下一轮beat
class Solution {
public:
ListNode* mergeKLists(vector<ListNode*>& lists) {//返回函数
return judge(lists, 0, lists.size() - 1);//创建一个包囊所有人的擂台
}
ListNode* judge(vector<ListNode*>& lists, int left, int right) //淘汰递归赛制
{
if (left == right)
return lists[left];//递归出口,(最底层的各个擂台成员)
if (left > right)
return NULL;
int mid = (right - left) / 2 + left;
return judge2(judge(lists,left,mid), judge(lists, mid+1, right));//二分法保证
//各个人都可以拥有参赛资格
}
ListNode* judge2(ListNode* a, ListNode* b)
{
ListNode head, * tail = &head, * ap = a, * bp = b;
if (!a)return b;
if (!b)return a;
while (ap && bp)//一场淘汰赛的淘汰机制
{
if (ap->val > bp->val)
{
tail->next = bp;
tail = tail->next;
bp = bp->next;
}
else
{
tail->next = ap;
tail = tail->next;
ap = ap->next;
}
}
tail->next = ap ? ap : bp;//赢得让对方余下的全部加入自己
return head.next;
}
};
3.优先队列
这个也属于超纲了hh不过刚刚上网查看懂了,只能说没想到还有这种方便的容器.
看看今天能写到哪里,如果今晚还有时间再来写题解吧
leetcode上看到的,十分直观.
2. 两数相加
给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。
请你将两个数相加,并以相同形式返回一个表示和的链表。
你可以假设除了数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入: l1 = [2,4,3], l2 = [5,6,4] 输出: [7,0,8] 解释: 342 + 465 = 807.示例 2:
输入: l1 = [0], l2 = [0] 输出: [0]示例 3:
输入: l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] 输出: [8,9,9,9,0,0,0,1]
这道题就比较经典了,而且因为这道题是我刷的第一道链表题让我记忆犹新,尤其是要注意的最后进位问题以及空链表问题.
暴力
//代码参考了leetcode官方的方案,因为当时那个空节点实整的我实在是头疼,一气之下把自己
//原来快完成的代码全删了233.所以现在扒出来既是为了提醒自己也是为了巩固基础
class Solution {
public:
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
int carry = 0;
ListNode* head =NULL;
ListNode* n = NULL;
while (l1 || l2)
{
int n1 = l1 ? l1->val : 0;//防止NULL,必须初始化
int n2 = l2 ? l2->val : 0;
int sum = (carry + n1 + n2)%10;//这个carry是上一代的
carry = (n1 + n2+carry) / 10;//刷新carry
if (!head)
{
head = n = new ListNode (sum);//最后要返回head,所以head也要创造节点
}
else {
n->next = new ListNode(sum);
n = n->next;
}
if (l1) {
l1 = l1->next;
}
if (l2) {
l2 = l2->next;
}
}
if (carry > 0) {//最后一个判断进位问题
n->next = new ListNode(carry);}
return head;
}
};
24. 两两交换链表中的节点
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。
示例 1:
输入: head = [1,2,3,4] 输出: [2,1,4,3]示例 2:
输入: head = [] 输出: []示例 3:
输入: head = [1] 输出: [1]
其实刚拿到这道题的第一反应是双指针,但想想发现这是链表,不能往回走.于是立刻想到堆栈(我也不知道为什么hh,效果也不算很好,但还是做出来了).
1.堆栈(其实完全可以用其他方法)
class Solution {
public:
ListNode* swapPairs(ListNode* head)
{
if (head == NULL||head->next==NULL)
return head;//从第二个节点开始
ListNode* temp =head->next ;
ListNode* j = NULL;//打工节点
stack<ListNode*>v;
while (head != NULL) {//因为是两两交换,故只有出栈和入栈两种情况
if (v.empty())
{
v.push(head);
head = head->next;
}
else
{
if (j != NULL)
j->next = head;//(2->)1->4(->3)
j = v.top();
v.top()->next = head->next;//让1->3
head->next = v.top();
head = v.top()->next;
v.pop();
}
}
return temp;//返回第二个节点
}
};
2.递归
神奇的递归,自然出自官方手.
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head;
}//一样的开头
ListNode* newHead = head->next;//head==1,newhead==2
head->next = swapPairs(newHead->next);//1->next==2->next(原,无next则3,有则4)
//用一个递归处理两个数值,所以这里传递的数值永远是奇数!
newHead->next = head;//2->next==1
return newHead;
}
};
从最小分配看,不过是让2->1且1->?; " "?当没有->则返回?,有则返回一个?->next且指向?也就是创建了2->1->的链表,后面的->是递归的灵魂.他创造了一个链接.
所以子问题是2->1后怎么创建这个->,以及怎么创建2->1;
出口就是不断减小的范围(正序递归)
3.迭代
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* dummyHead = new ListNode(0);
dummyHead->next = head;
ListNode* temp = dummyHead;
while (temp->next != nullptr && temp->next->next != nullptr) {//考虑最后两
//个节点是否满足一对
ListNode* node1 = temp->next;//1//2
ListNode* node2 = temp->next->next;//2//4
temp->next = node2;//head->2//1->4
node1->next = node2->next;//1->3//3->NULL(5)
node2->next = node1;//2->1//4->3
temp = node1;//temp(head)=1.//temp=2
}
return dummyHead->next;
}
};
迭代的本质和递归,以及我的栈都是一样的思想,一轮处理两个数据,并且考虑前两个是否满足一对.(迭代也有,只是用while代替了)
3.后话
其实要总结的还有很多,像本来树我打算和链表一起总结,但可惜没时间了.第一次写自己的博客就花了一个下午4个多小时.不过比起一下午两道leetcode好多了hh还有字符串啊数组啊这些简单但当初也花了我不少时间的大大小小的玩意.刷了一个月leetcode也就刷了大概30道左右,平均45分钟一道(其实很多都是有思路但写不出来或者暴力后不满意,以及大佬的题解看不懂),毕竟还没学嘛.希望下次总结时我能写完自己的第一个小项目(比如贪吃蛇简单版)或者把黑马的两个项目写完,并且算法也能有序提高着.就这样吧,恰饭