20261.23 &1.24学习笔记

1.代码题

今日的代码题分别为24. 两两交换链表中的节点 206.反转链表 24. 两两交换链表中的节点 19.删除链表的倒数第N个节点 142.环形链表II

206.反转链表

参考视频:https://www.bilibili.com/video/BV1nB4y1i7eL?t=951.1

参考文档:206.反转链表 | 代码随想录

力扣题目链接:206. 反转链表 - 力扣(LeetCode)

题目:

题意:反转一个单链表。

示例: 输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL

思路

如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。

其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:

之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。

那么接下来看一看是如何反转的呢?

我们拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur)

首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。

然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。

为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。

接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。

最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。

#双指针法
复制代码
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        ListNode* temp; // 保存cur的下一个节点
        ListNode* cur = head;
        ListNode* pre = NULL;
        while(cur) {
            temp = cur->next;  // 保存一下 cur的下一个节点,因为接下来要改变cur->next
            cur->next = pre; // 翻转操作
            // 更新pre 和 cur指针
            pre = cur;
            cur = temp;
        }
        return pre;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)
#递归法

递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。

关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。

具体可以看代码(已经详细注释),双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。

复制代码
class Solution {
public:
    ListNode* reverse(ListNode* pre,ListNode* cur){
        if(cur == NULL) return pre;
        ListNode* temp = cur->next;
        cur->next = pre;
        // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步
        // pre = cur;
        // cur = temp;
        return reverse(cur,temp);
    }
    ListNode* reverseList(ListNode* head) {
        // 和双指针法初始化是一样的逻辑
        // ListNode* cur = head;
        // ListNode* pre = NULL;
        return reverse(NULL, head);
    }

};
  • 时间复杂度: O(n), 要递归处理链表的每个节点
  • 空间复杂度: O(n), 递归调用了 n 层栈空间

我们可以发现,上面的递归写法和双指针法实质上都是从前往后翻转指针指向,其实还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向。

具体代码如下(带详细注释):

复制代码
class Solution {
public:
    ListNode* reverseList(ListNode* head) {
        // 边缘条件判断
        if(head == NULL) return NULL;
        if (head->next == NULL) return head;
        
        // 递归调用,翻转第二个节点开始往后的链表
        ListNode *last = reverseList(head->next);
        // 翻转头节点与第二个节点的指向
        head->next->next = head;
        // 此时的 head 节点为尾节点,next 需要指向 NULL
        head->next = NULL;
        return last;
    }
}; 
  • 时间复杂度: O(n)
  • 空间复杂度: O(n)

注意事项:

1.该题首先要掌握的是双指针法,递归法第一遍看应该会很晕,但本质就是按照双指针法的方法完成的,若非面试官强制要求,熟练掌握双指针法即可

2.注意使用虚拟头结点的好处,本题的pre实际位置为虚拟头结点,但是由于链表末尾为null,所以虚拟头结点直接设为null即可,while循环停止的条件实际为cur指向null时停止,所以直接while(cur)即可

24. 两两交换链表中的节点

参考视频:https://www.bilibili.com/video/BV1YT411g7br?t=848.8

参考文档:24. 两两交换链表中的节点 | 代码随想录

力扣题目链接:24. 两两交换链表中的节点 - 力扣(LeetCode)

题目

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

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

思路

这道题目正常模拟就可以了。

建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。

对虚拟头结点的操作,还不熟悉的话,可以看这篇链表:听说用虚拟头节点会方便很多? (opens new window)

接下来就是交换相邻两个元素了,此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序

初始时,cur指向虚拟头结点,然后进行如下三步:

操作之后,链表如下:

看这个可能就更直观一些了:

对应的C++代码实现如下: (注释中详细和如上图中的三步做对应)

复制代码
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点
        dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作
        ListNode* cur = dummyHead;
        while(cur->next != nullptr && cur->next->next != nullptr) {
            ListNode* tmp = cur->next; // 记录临时节点
            ListNode* tmp1 = cur->next->next->next; // 记录临时节点

            cur->next = cur->next->next;    // 步骤一
            cur->next->next = tmp;          // 步骤二
            cur->next->next->next = tmp1;   // 步骤三

            cur = cur->next->next; // cur移动两位,准备下一轮交换
        }
        ListNode* result = dummyHead->next;
        delete dummyHead;
        return result;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

注意事项:

1.代码中while(cur->next != nullptr && cur->next->next != nullptr)是否可以更换位置?

答案是不可以,因为如果先执行后半句cur->next->next != nullptr则可能因为cur->next 为 nullptr而未正确判断导致空指针的错误使用,所以这句话的顺序是不可以换的

2.为什么要新建temp temp1俩个临时指针?

如上图,当我们进行第一步时候,删除了cur->结点1,那么当cur->结点2后我们无法正确找到结点1的地址,所以此时需要先创建一个新的临时指针保存结点1的地址,即temp = cur -> next,同理 当我们执行第二步时候,结点2->结点1则无法争取找到结点3的地址,所以要先建立临时指针temp1保存结点3的地址,防止无法正确找到

3.在交换完成之前,cur的位置都是不会变的,所以最好画图以便更加容易看出cur所指向的位置

以下是笔者的代码

复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* swapPairs(ListNode* head) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead ->next = head;
        ListNode *cur = dummyhead;
        while(cur ->next!= nullptr && cur ->next ->next !=nullptr){
            ListNode* temp = cur ->next;
            ListNode* temp1 = cur -> next ->next ->next;
            cur->next = cur ->next ->next;
            cur ->next ->next = temp; //此处需要注意cur位置还未变动
            temp ->next = temp1;
            cur = cur ->next ->next;//cur向后移俩位
        }
        return dummyhead ->next;
    }
};

19.删除链表的倒数第N个节点

参考视频:https://www.bilibili.com/video/BV1vW4y1U7Gf?t=444.8

参考文档:19.删除链表的倒数第N个节点 | 代码随想录

力扣题目链接:19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

题目:

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶:你能尝试使用一趟扫描实现吗?

示例 1:

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

示例 2:

输入:head = [1], n = 1 输出:[]

示例 3:

输入:head = [1,2], n = 1 输出:[1]

思路

双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。

思路是这样的,但要注意一些细节。

分为如下几步:

  • fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图:

  • fast和slow同时移动,直到fast指向末尾,如题:

    //图片中有错别词:应该将"只到"改为"直到"

  • 删除slow指向的下一个节点,如图:

此时不难写出如下C++代码:

复制代码
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyHead = new ListNode(0);
        dummyHead->next = head;
        ListNode* slow = dummyHead;
        ListNode* fast = dummyHead;
        while(n-- && fast != NULL) {
            fast = fast->next;
        }
        fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点
        while (fast != NULL) {
            fast = fast->next;
            slow = slow->next;
        }
        slow->next = slow->next->next; 
        
        // ListNode *tmp = slow->next;  C++释放内存的逻辑
        // slow->next = tmp->next;
        // delete tmp;
        
        return dummyHead->next;
    }
};
  • 时间复杂度: O(n)
  • 空间复杂度: O(1)

注意事项:

1.先要判断fast是否会为空指针,并让fast指针先向前走n步,但由于我们要找到被删除的倒数第n个结点的前一个结点,所以可以让fast先走n+1步后再让fast 和slow指针同时向后走,方便做删除操作,而让fast先走n+1步有俩种操作方式,一种如上,但是可能会在执行fast = fast->next;碰到空指针错误,所以可以如下写

复制代码
n++; 
while(n-- && fast != NULL) {
            fast = fast->next;
        }

以下是笔者的代码:

复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode() : val(0), next(nullptr) {}
 *     ListNode(int x) : val(x), next(nullptr) {}
 *     ListNode(int x, ListNode *next) : val(x), next(next) {}
 * };
 */
class Solution {
public:
    ListNode* removeNthFromEnd(ListNode* head, int n) {
        ListNode* dummyhead = new ListNode(0);
        dummyhead -> next = head;
        ListNode* fast = dummyhead;
        ListNode* slow = dummyhead;
        
        while(n-- && fast -> next != nullptr ){
            fast = fast ->next;
        }
        fast= fast->next;
        while(fast != nullptr){
            fast = fast ->next;
            slow = slow ->next;
        }

        if(slow->next != nullptr){
            slow ->next = slow ->next ->next;
        }
        
        return dummyhead->next;
    }
};

142.环形链表II

参考视频:https://www.bilibili.com/video/BV1if4y1d7ob?t=1167.2

参考文档:142.环形链表II | 代码随想录

力扣题目链接:142. 环形链表 II - 力扣(LeetCode)

题目:

题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。

说明:不允许修改给定的链表。

思路

这道题目,不仅考察对链表的操作,而且还需要一些数学运算。

主要考察两知识点:

  • 判断链表是否环
  • 如果有环,如何找到这个环的入口

#判断链表是否有环

可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。

为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢

首先第一点:fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。

那么来看一下,为什么fast指针和slow指针一定会相遇呢?

可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。

会发现最终都是这种情况, 如下图:

fast和slow各自再走一步, fast和slow就相遇了

这是因为fast是走两步,slow是走一步,其实相对于slow来说,fast是一个节点一个节点的靠近slow的,所以fast一定可以和slow重合。

动画如下:

#如果有环,如何找到这个环的入口

此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。

假设从头结点到环形入口节点 的节点数为x。 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示:

那么相遇时: slow指针走过的节点数为: x + y, fast指针走过的节点数:x + y + n (y + z),n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。

因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2:

(x + y) * 2 = x + y + n (y + z)

两边消掉一个(x+y): x + y = n (y + z)

因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。

所以要求x ,将x单独放在左面:x = n (y + z) - y ,

再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:x = (n - 1) (y + z) + z 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。

这个公式说明什么呢?

先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。

当 n为1的时候,公式就化解为 x = z

这就意味着,从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点

也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。

让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。

动画如下:

那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。

其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。

代码如下:

复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode* fast = head;
        ListNode* slow = head;
        while(fast != NULL && fast->next != NULL) {
            slow = slow->next;
            fast = fast->next->next;
            // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
            if (slow == fast) {
                ListNode* index1 = fast;
                ListNode* index2 = head;
                while (index1 != index2) {
                    index1 = index1->next;
                    index2 = index2->next;
                }
                return index2; // 返回环的入口
            }
        }
        return NULL;
    }
};
  • 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n
  • 空间复杂度: O(1)

补充

在推理过程中,大家可能有一个疑问就是:为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?

即文章链表:环找到了,那入口呢? (opens new window)中如下的地方:

首先slow进环的时候,fast一定是先进环来了。

如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子:

可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。

重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图:

那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。

因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。

也就是说slow一定没有走到环入口3,而fast已经到环入口3了

这说明什么呢?

在slow开始走的那一环已经和fast相遇了

那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,fast相对于slow是一次移动一个节点,所以不可能跳过去

好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对链表:环找到了,那入口呢? (opens new window)的补充。

可以参考下面这张图:

本题up已经讲的很详细了,所以笔者在这就简单给出自己的代码

复制代码
/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode *detectCycle(ListNode *head) {
        ListNode * fast = head;
        ListNode * slow = head;
        while(fast != NULL && fast ->next != NULL){
            fast = fast ->next ->next;
            slow = slow ->next;
            if(fast == slow){
                ListNode* index1 = fast;
                ListNode* index2 = head;
                while(index1 != index2 ){
                index1 =index1 ->next;
                index2 = index2 ->next;
            }
            return index1;
            }
            
        }
        return NULL;


    }
};

同时附上代码随想录的链表总结,以便有助于大家更进一步复习该知识点

链表总结篇

#链表的理论基础

在这篇文章关于链表,你该了解这些! (opens new window)中,介绍了如下几点:

  • 链表的种类主要为:单链表,双链表,循环链表
  • 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。
  • 链表是如何进行增删改查的。
  • 数组和链表在不同场景下的性能分析。

可以说把链表基础的知识都概括了,但又不像教科书那样的繁琐

#链表经典题目

#虚拟头结点

链表:听说用虚拟头节点会方便很多? (opens new window)中,我们讲解了链表操作中一个非常重要的技巧:虚拟头节点。

链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。

每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题

链表:听说用虚拟头节点会方便很多? (opens new window)中,我给出了用虚拟头结点和没用虚拟头结点的代码,大家对比一下就会发现,使用虚拟头结点的好处。

#链表的基本操作

链表:一道题目考察了常见的五个操作! (opens new window)中,我们通过设计链表把链表常见的五个操作练习了一遍。

这是练习链表基础操作的非常好的一道题目,考察了:

  • 获取链表第index个节点的数值
  • 在链表的最前面插入一个节点
  • 在链表的最后面插入一个节点
  • 在链表第index个节点前面插入一个节点
  • 删除链表的第index个节点的数值

可以说把这道题目做了,链表基本操作就OK了,再也不用担心链表增删改查整不明白了

这里我依然使用了虚拟头结点的技巧,大家复习的时候,可以去看一下代码。

#反转链表

链表:听说过两天反转链表又写不出来了? (opens new window)中,讲解了如何反转链表。

因为反转链表的代码相对简单,有的同学可能直接背下来了,但一写还是容易出问题。

反转链表是面试中高频题目,很考察面试者对链表操作的熟练程度。

我在文章 (opens new window)中,给出了两种反转的方式,迭代法和递归法。

建议大家先学透迭代法,然后再看递归法,因为递归法比较绕,如果迭代还写不明白,递归基本也写不明白了。

可以先通过迭代法,彻底弄清楚链表反转的过程!

#删除倒数第N个节点

链表:删除链表倒数第N个节点,怎么删? (opens new window)中我们结合虚拟头结点 和 双指针法来移除链表倒数第N个节点。

#链表相交

链表:链表相交 (opens new window)使用双指针来找到两个链表的交点(引用完全相同,即:内存地址完全相同的交点)

#环形链表

链表:环找到了,那入口呢? (opens new window)中,讲解了在链表如何找环,以及如何找环的入口位置。

这道题目可以说是链表的比较难的题目了。 但代码却十分简洁,主要在于一些数学证明。

#总结

这个图是 代码随想录知识星球 (opens new window)成员:海螺人 (opens new window),所画,总结的非常好,分享给大家。

考察链表的操作其实就是考察指针的操作,是面试中的常见类型。

链表篇中开头介绍链表理论知识 (opens new window),然后分别通过经典题目介绍了如下知识点:

  1. 关于链表,你该了解这些!(opens new window)
  2. 虚拟头结点的技巧(opens new window)
  3. 链表的增删改查(opens new window)
  4. 反转一个链表(opens new window)
  5. 删除倒数第N个节点(opens new window)
  6. 链表相交(opens new window)
  7. 有否环形,以及环的入口

2.c++学习

前言

配套视频:https://www.bilibili.com/video/BV1et411b73Z

只是为方便学习,不做其他用途,在此发布C++基础入门部分配套讲义,原作者为黑马程序

参考文档:第3阶段-C++核心编程 资料/讲义/C++核心编程.md · 赤伶/Cpp-0-1-Resource - 码云 - 开源中国

由于之前不知为何视频跳了很多,今天笔者学习的是引用和函数提高,并在相应出给出笔者的学习心得

内存分区模型

C++程序在执行时,将内存大方向划分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理的
  • 全局区:存放全局变量和静态变量以及常量
  • 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
  • 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

内存四区意义:

不同区域存放的数据,赋予不同的生命周期, 给我们更大的灵活编程

1.1 程序运行前

​ 在程序编译后,生成了exe可执行程序,未执行该程序前分为两个区域

代码区:

​ 存放 CPU 执行的机器指令

​ 代码区是共享的,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可

​ 代码区是只读的,使其只读的原因是防止程序意外地修改了它的指令

全局区:

​ 全局变量和静态变量存放在此.

​ 全局区还包含了常量区, 字符串常量和其他常量也存放在此.

​ ==该区域的数据在程序结束后由操作系统释放==.

示例:

复制代码
//全局变量
int g_a = 10;
int g_b = 10;

//全局常量
const int c_g_a = 10;
const int c_g_b = 10;

int main() {

	//局部变量
	int a = 10;
	int b = 10;

	//打印地址
	cout << "局部变量a地址为: " << (int)&a << endl;
	cout << "局部变量b地址为: " << (int)&b << endl;

	cout << "全局变量g_a地址为: " <<  (int)&g_a << endl;
	cout << "全局变量g_b地址为: " <<  (int)&g_b << endl;

	//静态变量
	static int s_a = 10;
	static int s_b = 10;

	cout << "静态变量s_a地址为: " << (int)&s_a << endl;
	cout << "静态变量s_b地址为: " << (int)&s_b << endl;

	cout << "字符串常量地址为: " << (int)&"hello world" << endl;
	cout << "字符串常量地址为: " << (int)&"hello world1" << endl;

	cout << "全局常量c_g_a地址为: " << (int)&c_g_a << endl;
	cout << "全局常量c_g_b地址为: " << (int)&c_g_b << endl;

	const int c_l_a = 10;
	const int c_l_b = 10;
	cout << "局部常量c_l_a地址为: " << (int)&c_l_a << endl;
	cout << "局部常量c_l_b地址为: " << (int)&c_l_b << endl;

	system("pause");

	return 0;
}

打印结果:

总结:

  • C++中在程序运行前分为全局区和代码区
  • 代码区特点是共享和只读
  • 全局区中存放全局变量、静态变量、常量
  • 常量区中存放 const修饰的全局常量 和 字符串常量

1.2 程序运行后

栈区:

​ 由编译器自动分配释放, 存放函数的参数值,局部变量等

​ 注意事项:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放

示例:

复制代码
int * func()
{
	int a = 10;
	return &a;
}

int main() {

	int *p = func();

	cout << *p << endl;
	cout << *p << endl;

	system("pause");

	return 0;
}

堆区:

​ 由程序员分配释放,若程序员不释放,程序结束时由操作系统回收

​ 在C++中主要利用new在堆区开辟内存

示例:

复制代码
int* func()
{
	int* a = new int(10);
	return a;
}

int main() {

	int *p = func();

	cout << *p << endl;
	cout << *p << endl;
    
	system("pause");

	return 0;
}

总结:

堆区数据由程序员管理开辟和释放

堆区数据利用new关键字进行开辟内存

1.3 new操作符

​ C++中利用==new==操作符在堆区开辟数据

​ 堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 ==delete==

​ 语法: new 数据类型

​ 利用new创建的数据,会返回该数据对应的类型的指针

示例1: 基本语法

复制代码
int* func()
{
	int* a = new int(10);
	return a;
}

int main() {

	int *p = func();

	cout << *p << endl;
	cout << *p << endl;

	//利用delete释放堆区数据
	delete p;

	//cout << *p << endl; //报错,释放的空间不可访问

	system("pause");

	return 0;
}

示例2:开辟数组

复制代码
//堆区开辟数组
int main() {

	int* arr = new int[10];

	for (int i = 0; i < 10; i++)
	{
		arr[i] = i + 100;
	}

	for (int i = 0; i < 10; i++)
	{
		cout << arr[i] << endl;
	}
	//释放数组 delete 后加 []
	delete[] arr;

	system("pause");

	return 0;
}

2 引用

2.1 引用的基本使用

**作用: **给变量起别名

语法: 数据类型 &别名 = 原名

示例:

复制代码
int main() {

	int a = 10;
	int &b = a;

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	b = 100;

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;

	system("pause");

	return 0;
}

2.2 引用注意事项

  • 引用必须初始化
  • 引用在初始化后,不可以改变

示例:

复制代码
int main() {

	int a = 10;
	int b = 20;
	//int &c; //错误,引用必须初始化
	int &c = a; //一旦初始化后,就不可以更改
	c = b; //这是赋值操作,不是更改引用

	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;

	system("pause");

	return 0;
}

2.3 引用做函数参数

**作用:**函数传参时,可以利用引用的技术让形参修饰实参

**优点:**可以简化指针修改实参

示例:

复制代码
//1. 值传递
void mySwap01(int a, int b) {
	int temp = a;
	a = b;
	b = temp;
}

//2. 地址传递
void mySwap02(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

//3. 引用传递
void mySwap03(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

int main() {

	int a = 10;
	int b = 20;

	mySwap01(a, b);
	cout << "a:" << a << " b:" << b << endl;

	mySwap02(&a, &b);
	cout << "a:" << a << " b:" << b << endl;

	mySwap03(a, b);
	cout << "a:" << a << " b:" << b << endl;

	system("pause");

	return 0;
}

总结:通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单

2.4 引用做函数返回值

作用:引用是可以作为函数的返回值存在的

注意:不要返回局部变量引用

用法:函数调用作为左值

示例:

复制代码
//返回局部变量引用
int& test01() {
	int a = 10; //局部变量
	return a;
}

//返回静态变量引用
int& test02() {
	static int a = 20;
	return a;
}

int main() {

	//不能返回局部变量的引用
	int& ref = test01();
	cout << "ref = " << ref << endl;
	cout << "ref = " << ref << endl;

	//如果函数做左值,那么必须返回引用
	int& ref2 = test02();
	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	test02() = 1000;

	cout << "ref2 = " << ref2 << endl;
	cout << "ref2 = " << ref2 << endl;

	system("pause");

	return 0;
}

2.5 引用的本质

本质:引用的本质在c++内部实现是一个指针常量.

讲解示例:

复制代码
//发现是引用,转换为 int* const ref = &a;
void func(int& ref){
	ref = 100; // ref是引用,转换为*ref = 100
}
int main(){
	int a = 10;
    
    //自动转换为 int* const ref = &a; 指针常量是指针指向不可改,也说明为什么引用不可更改
	int& ref = a; 
	ref = 20; //内部发现ref是引用,自动帮我们转换为: *ref = 20;
    
	cout << "a:" << a << endl;
	cout << "ref:" << ref << endl;
    
	func(a);
	return 0;
}

结论:C++推荐用引用技术,因为语法方便,引用本质是指针常量,但是所有的指针操作编译器都帮我们做了

2.6 常量引用

**作用:**常量引用主要用来修饰形参,防止误操作

在函数形参列表中,可以加==const修饰形参==,防止形参改变实参

示例:

复制代码
//引用使用的场景,通常用来修饰形参
void showValue(const int& v) {
	//v += 10;
	cout << v << endl;
}

int main() {

	//int& ref = 10;  引用本身需要一个合法的内存空间,因此这行错误
	//加入const就可以了,编译器优化代码,int temp = 10; const int& ref = temp;
	const int& ref = 10;

	//ref = 100;  //加入const后不可以修改变量
	cout << ref << endl;

	//函数中利用常量引用防止误操作修改实参
	int a = 10;
	showValue(a);

	system("pause");

	return 0;
}

3 函数提高

3.1 函数默认参数

在C++中,函数的形参列表中的形参是可以有默认值的。

语法: 返回值类型 函数名 (参数= 默认值){}

示例:

复制代码
int func(int a, int b = 10, int c = 10) {
	return a + b + c;
}

//1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
int func2(int a = 10, int b = 10);
int func2(int a, int b) {
	return a + b;
}

int main() {

	cout << "ret = " << func(20, 20) << endl;
	cout << "ret = " << func(100) << endl;

	system("pause");

	return 0;
}

3.2 函数占位参数

C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置

语法: 返回值类型 函数名 (数据类型){}

在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术

示例:

复制代码
//函数占位参数 ,占位参数也可以有默认参数
void func(int a, int) {
	cout << "this is func" << endl;
}

int main() {

	func(10,10); //占位参数必须填补

	system("pause");

	return 0;
}

3.3 函数重载

3.3.1 函数重载概述

**作用:**函数名可以相同,提高复用性

函数重载满足条件:

  • 同一个作用域下
  • 函数名称相同
  • 函数参数类型不同 或者 个数不同 或者 顺序不同

注意: 函数的返回值不可以作为函数重载的条件

示例:

复制代码
//函数重载需要函数都在同一个作用域下
void func()
{
	cout << "func 的调用!" << endl;
}
void func(int a)
{
	cout << "func (int a) 的调用!" << endl;
}
void func(double a)
{
	cout << "func (double a)的调用!" << endl;
}
void func(int a ,double b)
{
	cout << "func (int a ,double b) 的调用!" << endl;
}
void func(double a ,int b)
{
	cout << "func (double a ,int b)的调用!" << endl;
}

//函数返回值不可以作为函数重载条件
//int func(double a, int b)
//{
//	cout << "func (double a ,int b)的调用!" << endl;
//}


int main() {

	func();
	func(10);
	func(3.14);
	func(10,3.14);
	func(3.14 , 10);
	
	system("pause");

	return 0;
}
3.3.2 函数重载注意事项
  • 引用作为重载条件
  • 函数重载碰到函数默认参数

示例:

复制代码
//函数重载注意事项
//1、引用作为重载条件

void func(int &a)
{
	cout << "func (int &a) 调用 " << endl;
}

void func(const int &a)
{
	cout << "func (const int &a) 调用 " << endl;
}


//2、函数重载碰到函数默认参数

void func2(int a, int b = 10)
{
	cout << "func2(int a, int b = 10) 调用" << endl;
}

void func2(int a)
{
	cout << "func2(int a) 调用" << endl;
}

int main() {
	
	int a = 10;
	func(a); //调用无const
	func(10);//调用有const


	//func2(10); //碰到默认参数产生歧义,需要避免

	system("pause");

	return 0;
}

为了看着更加整洁,笔者的相应文件如下

相应的代码如下

目录

1.代码题

206.反转链表

思路

<#双指针法>

<#递归法>

[24. 两两交换链表中的节点](#24. 两两交换链表中的节点)

思路

19.删除链表的倒数第N个节点

思路

142.环形链表II

思路

<#判断链表是否有环>

#如果有环,如何找到这个环的入口

补充

链表总结篇

<#链表的理论基础>

<#链表经典题目>

<#虚拟头结点>

<#链表的基本操作>

<#反转链表>

#删除倒数第N个节点

<#链表相交>

<#环形链表>

<#总结>

2.c++学习

内存分区模型

[1.1 程序运行前](#1.1 程序运行前)

[1.2 程序运行后](#1.2 程序运行后)

[1.3 new操作符](#1.3 new操作符)

[2 引用](#2 引用)

[2.1 引用的基本使用](#2.1 引用的基本使用)

[2.2 引用注意事项](#2.2 引用注意事项)

[2.3 引用做函数参数](#2.3 引用做函数参数)

[2.4 引用做函数返回值](#2.4 引用做函数返回值)

[2.5 引用的本质](#2.5 引用的本质)

[2.6 常量引用](#2.6 常量引用)

[3 函数提高](#3 函数提高)

[3.1 函数默认参数](#3.1 函数默认参数)

[3.2 函数占位参数](#3.2 函数占位参数)

[3.3 函数重载](#3.3 函数重载)

[3.3.1 函数重载概述](#3.3.1 函数重载概述)

[3.3.2 函数重载注意事项](#3.3.2 函数重载注意事项)

func.h

func.cpp

函数.cpp

引用.cpp


复制代码
#pragma once
#include<iostream>
#include<string>
using namespace std;

//int* func(int b);

//int* func1();

void mySwap01(int a, int b);

void mySwap02(int* a, int* b);

void mySwap03(int& a, int& b);

int* func2();

//void test01();
//
//void test02();

void showValue(const int& v);

func.cpp

复制代码
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
using namespace std;

//栈区数据注意事项 -- 不要返回局部变量的地址
//栈区的数据由编译器管理开辟和释放

int* func(int b) //形参数据也会放在栈区
{
	b = 100;
	int a = 10;  //局部变量  存放在栈区  栈区的数据在函数执行完后自动释放
	return &a;  //返回局部变量的地址
}

int* func1()
{
	//利用new 关键字 可以将数据开辟到堆区
	//指针本质也是局部变量 放在栈上 指针保存的数据是放在堆区
	int* a = new int(10);
	return a;
}
//交换函数

//值传递
void mySwap01(int a, int b) {
	int temp = a;
	a = b;
	b = temp;
}

//2. 地址传递
void mySwap02(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

//3. 引用传递
void mySwap03(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

//new 的基本语法
int* func2()
{
	//在堆区创建整型数据
	//new返回的是 该数据类型的指针;
	int* p = new int(10);

	return p;
}

//void test01() {
//	int* p = func2();
//	cout << *p << endl;
//	//堆区的数据由程序员管理开辟,程序员管理释放
//	//如果想释放堆区的数据 利用关键字delete
//	delete p;
//
//	//cout << *p << endl; //内存已经被释放 在此访问就是非法操作,会报错
//}
//
////在堆区利用new开辟数组
//
//void test02() {
//	//创建10整型数据的数组,在堆区
//	int* arr = new int[10];//10代表数组有10个元素
//
//	for (int i = 0; i < 10; i++)
//	{
//		arr[i] = i + 100; //给10个元素赋值100-109
//	}
//
//	for (int i = 0; i < 10; i++)
//	{
//		cout << arr[i] << endl;
//	}
//	//释放数组 delete 后加 []
//	delete[] arr;
//}

//打印数据函数
void showValue(const int& v) {
	//v += 10;
	cout << v << endl;
}

函数.cpp

复制代码
//#define _CRT_SECURE_NO_WARNINGS 1
//#include<iostream>
//#include<string>
//using namespace std;
//#include "func.h"
//
//
//
////函数默认参数
////在C++中,函数的形参列表中的形参是可以有默认值的。
////如果我们自己传入数据,就用自己的数据 如果没有,就用默认值
//
////int func(int a, int b = 10, int c = 10) {
////	return a + b + c;
////}
////
//////注意事项
//////1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
//////int func2(int a = 10, int b );
////
//////2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
//////声明和实现智能有一个有默认参数
////int func2(int a, int b);
////int func2(int a, int b) {
////	return a + b;
////}
//
////函数占位参数
////C++中函数的形参列表里可以有占位参数,用来做占位,调用函数时必须填补该位置
////
////语法: 返回值类型 函数名(数据类型) {}
////
////在现阶段函数的占位参数存在意义不大,但是后面的课程中会用到该技术
//
//
////函数重载
////3.3.1 函数重载概述
////* *作用: * *函数名可以相同,提高复用性
////
////函数重载满足条件:
////
////同一个作用域下
////函数名称相同
////函数参数类型不同 或者 个数不同 或者 顺序不同
////注意 : 函数的返回值不可以作为函数重载的条件
//
////
////void func()
////{
////	cout << "func 的调用!" << endl;
////}
////void func(int a)
////{
////	cout << "func (int a) 的调用!" << endl;
////}
////void func(double a)
////{
////	cout << "func (double a)的调用!" << endl;
////}
////void func(int a, double b)
////{
////	cout << "func (int a ,double b) 的调用!" << endl;
////}
////void func(double a, int b)
////{
////	cout << "func (double a ,int b)的调用!" << endl;
////}
//
////函数重载注意事项
////1、引用作为重载条件
//
//void func(int& a)
//{
//	cout << "func (int &a) 调用 " << endl;
//}
//
//void func(const int& a)
//{
//	cout << "func (const int &a) 调用 " << endl;
//}
//
//
////2、函数重载碰到函数默认参数
//
//void func2(int a, int b = 10)
//{
//	cout << "func2(int a, int b = 10) 调用" << endl;
//}
//
//void func2(int a)
//{
//	cout << "func2(int a) 调用" << endl;
//}
//
//
//int main() {
//
//
//	//cout << "ret = " << func(20, 20) << endl;
//	//cout << "ret = " << func(20) << endl;
//	//cout << "ret = " << func2(100) << endl;//2. 如果函数声明有默认值,函数实现的时候就不能有默认参数
//
//	//func(); //占位参数必须填补
//
//	//func();
//	//func(10);
//	//func(3.14);
//	//func(10, 3.14);
//	//func(3.14, 10);
//
//
//	int a = 10;
//	func(a); //调用无const
//	func(10);//调用有const
//
//	system("pause");
//
//	return 0;
//}

引用.cpp

复制代码
//#define _CRT_SECURE_NO_WARNINGS 1
//#include<iostream>
//#include<string>
//using namespace std;
//#include "func.h"
//
////全局变量
//int g_a = 10;
//int g_b = 10;
//
////cosnt 修饰的全局变量  全局常量
//const int c_g_a = 10;
//const int c_g_b = 20;
//
//
////返回局部变量引用
//int& test01() {
//	int a = 10; //局部变量
//	return a;
//}
//
////返回静态变量引用
//int& test02() {
//	static int a = 20; //静态变量,存放在全局区 全局区上的数据在程序结束后系统释放
//	return a;
//}
//
//
//int main() {
//
//	////全局区
//
//	////全局变量,静态变量,常量
//
//	////常见普通局部变量
//	//int a = 10;
//	//int b = 10;
//	//cout << "局部变量a的地址为" << (int)&a << endl;
//	//cout << "局部变量b的地址为" << (int)&b << endl;
//
//
//	//cout << "全局变量g_a的地址为" << (int)&g_a << endl;
//	//cout << "全局变量g_b的地址为" << (int)&g_b << endl;
//
//	////静态变量  在普通变量前面加static 属于静态变量  也存放在全局区中
//	//static int s_a = 10;
//	//static int s_b = 10;
//
//	//cout << "静态变量s_a的地址为" << (int)&s_a << endl;
//	//cout << "静态变量s_b的地址为" << (int)&s_b << endl;
//	//
//	////常量
//	////字符串常量  也放在全局区
//	//cout << "字符串常量的地址为" << (int)&"hello word" << endl;
//
//	////const修饰的变量
//	////const 修饰的全局变量   放在全局区
//	//// const 修饰的局部变量   放在常量区
//	//cout << "全局常量c_g_a的地址为" << (int)&c_g_a << endl;
//	//cout << "全局常量c_g_b的地址为" << (int)&c_g_b << endl;
//
//	//int c_l_a = 10;//c-const g-global l-local
//	//int c_l_b = 10;
//	//cout << "局部常量c_l_a的地址为" << (int)&c_l_a << endl;
//	//cout << "局部常量c_l_b的地址为" << (int)&c_l_b << endl;
//
//
//	//栈区数据注意事项 -- 不要返回局部变量的地址
//	////接受func函数的返回值
//	//int* p = func(1);
//
//	//cout << *p << endl;  //第一次可以正确打印正确的数字,是因为编译器做了保留
//	//cout << *p << endl;	//第二次这个数据就不再保留了  如果俩次都是10 可以选x86编译一下就会出错
//
//	//利用new 关键字 可以将数据开辟到堆区
//	//int* p = func1();
//
//	//cout << *p << endl;
//	//cout << *p << endl;
//
//	//new 的基本语法
//	//在堆区利用new开辟数组
//
//	//test02();
//
//
//	////引用基本语法
//	////数据类型&别名 =原名
//	//int a = 10;
//	////创建引用
//	//int& b = a; 
//
//	//cout << "a = " << a << endl;
//	//cout << "b = " << b << endl;
//
//	//b = 100;
//
//	//cout << "a = " << a << endl;
//	//cout << "b = " << b << endl;
//
//	////引用注意事项
//	////引用必须初始化
//	////引用在初始化后,不可以改变
//	//int a = 10;
//	//int b = 20;
//	////int &c; //错误,引用必须初始化
//	//int& c = a; //一旦初始化后,就不可以更改
//	//c = b; //这是赋值操作,不是更改引用
//
//	//cout << "a = " << a << endl;
//	//cout << "b = " << b << endl;
//	//cout << "c = " << c << endl;
//
//
//	//int a = 10;
//	//int b = 20;
//
//	//mySwap01(a, b);  //值传递 形参不会修饰实参
//	//cout << "swap01 a:" << a << "swap01 b:" << b << endl;
//
//	//mySwap02(&a, &b); //地址传递,形参会修饰实参的
//	//cout << "a:" << a << " b:" << b << endl;
//
//	//// 重置值,测试引用传递
//	//a = 10;
//	//b = 20;
//
//	////int& a 表示 a 是实参的 "别名",操作别名等价于操作实参本身,所以交换成功;
//	//mySwap03(a, b);//引用传递 形参会修饰实参的
//	//cout << "swap03 a:" << a << " swap03 b:" << b << endl;
//
//
//	//作用:引用是可以作为函数的返回值存在的
//	//
//	//注意:不要返回局部变量引用
//	//
//	//用法:函数调用作为左值
//
//	////不能返回局部变量的引用 是非法操作
//	//int& ref = test01();
//	//cout << "ref = " << ref << endl;
//	//cout << "ref = " << ref << endl;
//
//	////如果函数做左值,那么必须返回引用
//	//int& ref2 = test02();
//	//cout << "ref2 = " << ref2 << endl;
//	//cout << "ref2 = " << ref2 << endl;
//
//	//test02() = 1000; //如果函数的返回值是引用,这个函数调用可以作为左值
//
//	//cout << "ref2 = " << ref2 << endl;
//	//cout << "ref2 = " << ref2 << endl;
//
//	//引用的本质
//	//	本质:引用的本质在c++内部实现是一个指针常量.
//
//	//常量引用
//	//	** 作用:** 常量引用主要用来修饰形参,防止误操作
//
//	//	在函数形参列表中,可以加 == const修饰形参 == ,防止形参改变实参
//	//int a = 10;
//	//int& ref = 10; //引用必须引一块合法的内存空间
//	//加上const之后 编译器将代码修改 int temp = 10;const int &ref = temp;
//	//const int& ref = 10;
//	//ref = 20; //加入cosnt之后变为只读 不可以修改
//
//	int a = 1000;
//	showValue(a);
//
//	system("pause");
//
//	return 0;
//}

许多注意事项都在代码中,如读者有耐心可以跟着代码一步一步去试试,其中包括报错的代码和报错原因

++相信看到这的你也很努力,望与诸君共勉!!!++

相关推荐
晚风吹长发2 小时前
初步理解Linux中的信号概念以及信号产生
linux·运维·服务器·算法·缓冲区·inode
鱼跃鹰飞2 小时前
LeetCode热题100:5.最长回文子串
数据结构·算法·leetcode
tobias.b2 小时前
408真题解析-2010-10-数据结构-快速排序
java·数据结构·算法·计算机考研·408真题解析
季明洵2 小时前
力扣反转链表、两两交换链表中的节点、删除链表的倒数第N个节点
java·算法·leetcode·链表
历程里程碑2 小时前
Linux 4 指令结尾&&通过shell明白指令实现的原理
linux·c语言·数据结构·笔记·算法·排序算法
星河天欲瞩2 小时前
【深度学习Day4】线性代数基础
人工智能·深度学习·学习·线性代数
亲爱的非洲野猪2 小时前
动态规划进阶:树形DP深度解析
算法·动态规划·代理模式
lpfasd1232 小时前
《影响力》精读笔记
网络·笔记·成长
studyForMokey2 小时前
【跨端技术React Native】入门学习随笔记录
学习·react native·react.js