剑指offer刷题笔记-篇2

剑指offer

前言

主要刷题平台为 牛客网,部分题目使用 LeetCode 和 ACwing 作为辅助。每题均包含主要思路、详细注释、时间复杂度和空间复杂度分析,每题均是尽可能最佳的解决办法。

机器人的运动范围

机器人的运动范围

题目:

地上有一个 rows 行和 cols 列的方格。坐标从 [0,0] 到 [rows-1,cols-1] 。一个机器人从坐标 [0,0] 的格子开始移动,每一次只能向左,右,上,下四个方向移动一格,但是不能进入行坐标和列坐标的数位之和大于 threshold 的格子。 例如,当 threshold 为 18 时,机器人能够进入方格 [35,37] ,因为 3+5+3+7 = 18。但是,它不能进入方格 [35,38] ,因为 3+5+3+8 = 19 。请问该机器人能够达到多少个格子?

思路:

宽度优先遍历的经典题目
要注意判断障碍物,有些格子是不能走的

class Solution {
public:
    int get_signal_sum(int x)
    {
        int s=0;
        while(x)
        {
            s+=x%10;
            x/=10;
        }
        return s;
    }
    int get_sum(pair<int, int> &p)
    {
        return get_signal_sum(p.first)+get_signal_sum(p.second);
    }
    int movingCount(int threshold, int rows, int cols) {
        int res=0;
        if(rows==0||cols==0)
        {
            return 0;
        }
        
        //默认所有格子都是没有走过的
        vector<vector<bool>> st(rows,vector<bool>(cols));
        queue<pair<int, int>> q;    //宽度优先搜索使用的队列
        
        //也可以使用 make_pair
        q.push({0,0});
        
        //走的方向: 纵为x 横为y  上右下左
        int dx[4]={-1,0,1,0};
        int dy[4]={0,1,0,-1};
        //循环放入
        while(q.size())
        {
            auto t=q.front();
            q.pop();
            
            //如果不符合条件, 就不走这条路
            if(get_sum(t)>threshold||st[t.first][t.second]==true)
            {
                continue;
            }
            res++;
            st[t.first][t.second]=true;    //表示走过了
            
            //走的方向
            for(int i=0;i<4;i++)
            {
                int x=t.first+dx[i];
                int y=t.second+dy[i];
                
                //判断是否合法的坐标
                if(x>=0&&x<rows&&y>=0&&y<cols)
                {
                    q.push({x,y});
                }
            }
        }
        return res;
        
    }
};

每个节点最多只会入队一次,所以时间复杂度不会超过方格中的节点个数。
最坏情况下会遍历方格中的所有点,所以时间复杂度就是 O(nm)O(nm)。

剪绳子

剪绳子

题目:

给你一根长度为 n 的绳子,请把绳子剪成整数长的 m 段( m 、 n 都是整数, n > 1 并且 m > 1 , m <= n ),每段绳子的长度记为 k[1],…,k[m] 。请问 k [ 1 ] ∗ k [ 2 ] ∗ . . . ∗ k [ m ] k[1]*k[2]*...*k[m] k[1]k[2]...k[m] 可能的最大乘积是多少?例如,当绳子的长度是 8 时,我们把它剪成长度分别为 2、3、3 的三段,此时得到的最大乘积是 18 。

数据范围: 2 ≤ n ≤ 60 2 \le n \le 60 2n60
进阶:空间复杂度 O ( 1 ) O(1) O(1) ,时间复杂度 O ( n ) O(n) O(n)

思路:

先说结论:尽量分出多个3,其余为2,这样构成的数最大

下面证明:
N N N,可以划分为 N = n 1 + n 2 + . . . n k N=n_1+n_2+...n_k N=n1+n2+...nk
先假设存在 n i > = 5 n_i>= 5 ni>=5, 有 3 ∗ ( n i − 3 ) > = n i 3*(n_i-3)>=n_i 3(ni3)>=ni, 即 3 n i − 9 > = n i 3n_i-9>=n_i 3ni9>=ni, 就可以得到 2 n i > = 9 2n_i>=9 2ni>=9, 这是必然成立的,因为我们已经假设过 n i > = 5 n_i>= 5 ni>=5了,也就是说 如果最终解 中包含 大于等于5的数字,则一定可以拆分出 3,那么5就一定不是最优解了,下面最优解就落在 2 3 4 中了

下面假设 n i = 4 n_i=4 ni=4, 那么 n i = 2 ∗ 2 n_i=2*2 ni=22,所以可以使用 2 ∗ 2 2*2 22 来替代,所以最优解就一定在 2 和 3 之间了

而且有 2 ∗ 2 ∗ 2 < 3 ∗ 3 2*2*2<3*3 222<33, 即如果有三个以上的2,替换成3的乘积就一定更大,所以 2 的数量一定不会超过两个

选用尽量多的3,直到剩下2或者4时,用2。如果N模3余1就是两个2,如果模3余2就是一个2,如果模3余0就没有2

class Solution {
public:
    int cutRope(int number) {
        if(number<=3)
        {
            return 1*(number-1);
        }
        
        int res=1;
        if(number%3==1)
        {
            //得到两个2
            res*=4;
            number-=4;
        }
        if(number%3==2)
        {
            //得到一个2
            res*=2;
            number-=2;
        }
        //这个循环可以省掉
        if(number%3==0)
        {
            //没有2
           res*=3;
            number-=3;
        }
        
        //其余全是3
        while(number)
        {
            res*=3;
            number-=3;
        }
        
        return res;
        
        
    }
};

有一定的数学基础很重要

时间复杂度分析:当 n n n 比较大时, n n n 会被拆分成 ⌈ n / 3 ⌉ ⌈n/3⌉ n/3 个数,我们需要计算这么多次减法和乘法,所以时间复杂度是 O ( n ) O(n) O(n)

二进制中1的个数

二进制中1的个数

题目:

输入一个整数 n ,输出该数32位二进制表示中1的个数。其中负数用补码表示。

数据范围: − 2 31 < = n < = 2 31 - 2^{31} <= n <= 2^{31} 231<=n<=231

即范围为: − 2147483648 < = n < = 2147483647 -2147483648<= n <= 2147483647 2147483648<=n<=2147483647

思路:

感觉这题主要是在考察语言的基本知识,对于 位移操作,二进制的表示一定要熟悉

class Solution {
public:
     int  NumberOf1(int n) {
         //将负数转换为整数,变为无符号数,二进制不变化,但是在解释这个二进制的时候就是以无符号解释的
         unsigned int _n=n;
         
         //因为在C++ 中如果是负数就会在每次右移后 在最高位补1,为了防止 补1,就事先将其转化为 无符号数了
         int s=0;    //无符号数最高位 补0
         while(_n)
         {
             //每次取出个位,如果是1就加,如果不是就不加
             s+=_n&1;
             //右移一位,去掉个位
             _n>>=1;
         }
         return s;
     }
};

数值的整数次方

数值的整数次方

题目:

实现函数 double Power(double base, int exponent),求base的exponent次方。

注意:

1.保证base和exponent不同时为0。

2.不得使用库函数,同时不需要考虑大数问题

3.有特殊判题,不用考虑小数点后面0的位数。

数据范围: $|base| \le 100 $ , ∣ e x p o n e n t ∣ ≤ 100 |exponent| \le 100 exponent100,保证最终结果一定满足 ∣ v a l ∣ ≤ 1 0 4 |val| \le 10^4 val104
进阶:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)

思路:

下面这个算法的 时间复杂度 和 空间复杂度 都是满足要求的,也是比较简单和容易实现的

 class Solution {
public:
    double Power(double base, int exponent) {
        //用res 来实现 求指数
        double res=1;
        //防止 exponent 为负数
        for(int i=0;i<abs(exponent);i++)
        {
            res*=base;           
        }
        //如果 指数 是负数,就要将其变为 分数输出
        if(exponent<0)
        {
            res = 1/res;
        }
        return res;
    }
};

空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)

从1到n的位数

输出每一个数字

题目:

输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。

  1. 用返回一个整数列表来代替打印
  2. n 为正整数,0 < n <= 5

感觉这一题简单的莫名其妙的

简单是由于 牛客网 的题目限制了 输入范围,导致不会出现大数溢出的问题,如果考虑大数移除的问题,就是如 力扣 的要求:最大的n位数 ,就需要使用 数组模拟遍历了

class Solution {
public:
 vector<int> output;

 void permutation(string& s, int length, int pos)
 {
     // 对带有循环的递归有时候还挺绕的
     if(pos == length)
     {
         //达到位数要求了
        inputNumbers(s);
        return; 
     }
     for(int i=0; i<=9; ++i)
     {
         //对于每一位 每个数字都有可能
         s[pos] = i + '0';
         permutation(s, length, pos + 1);
     }
 }
 void inputNumbers(string s)
 {
     // 放入答案
     bool isUnwantedZero = true;
     string temp = "";
     for(int i=0; i<s.length(); ++i)
     {
         if(isUnwantedZero && s[i] != '0') isUnwantedZero = false;
         if(!isUnwantedZero) temp += s[i];
     }
     //转化为数字放入
     if(temp != "") output.push_back(stoi(temp)); // 如果 s = "000",则temp会是空,stoi无法执行,会报错
 }

 vector<int> printNumbers(int n) {

     if(n <= 0) return vector<int>(0);

     //对于大数的求解
     string s(n, '0');
     for(int i=0; i<=9; ++i)
     {
         //int-> char 的转换
         s[0] = i + '0';
         permutation(s, s.length(), 1);
     }
     return output; 

 }
};
 class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param n int整型 最大位数
     * @return int整型vector
     */
    vector<int> printNumbers(int n) {
        // write code here
        vector<int> result;
        if(n==1)
        {
            for(int i=1;i<=9;i++)
            {
                result.push_back(i);
            }
        }
        else if(n==2)
        {
            for(int i=1;i<=99;i++)
            {
                result.push_back(i);
            }
        }
        else if(n==3)
        {
            for(int i=1;i<=999;i++)
            {
                result.push_back(i);
            }
        }
        else if(n==4)
        {
            for(int i=1;i<=9999;i++)
            {
                result.push_back(i);
            }
        }
        else
        {
            for(int i=1;i<=99999;i++)
            {
                result.push_back(i);
            }
        }
        return result;
    }
};

移除链表元素

移除链表元素

题目:

给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。返回删除后的链表的头节点。

1.此题对比原题有改动

2.题目保证链表中节点的值互不相同

3.该题只会输出返回的链表和结果做对比,所以若使用 C 或 C++ 语言,你不需要 free 或 delete 被删除的节点

数据范围:

0<=链表节点值<=10000

0<=链表长度<=10000

思路:

这里的要求是降低了好多的,没有让 再释放内存,感觉还是力扣的很不错: 移除链表元素

还有要注意一点就是 使用虚拟头结点 要注意在进循环的时候是 cur -> next !=nullptr

 /**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param head ListNode类 
     * @param val int整型 
     * @return ListNode类
     */
    ListNode* deleteNode(ListNode* head, int val) {
        // write code here
        ListNode* dummyHead=new ListNode(0);
        dummyHead->next=head;
        
        ListNode* cur=dummyHead;
        //ListNode* tmp=nullptr;
        while(cur->next!=nullptr)
        {
            //tmp=cur->next;
            if(cur->next->val==val)
            {
                cur->next=cur->next->next;
                //delete tmp;
            }
            else
            {
                cur=cur->next;
            }
            
        }
        return dummyHead->next;
    }
};

acwing类似题目: O ( 1 ) O(1) O(1)时间内删除节点​

给定单向链表的一个节点指针,定义一个函数在O(1)时间删除该结点。

假设链表一定存在,并且该节点一定不是尾节点

个人感觉这是一种非常取巧的方法,由于只有一个节点,而且是单向链表,无法获取 前驱结点,所以就采用了非常巧妙的办法:既然当前节点需要使用到 前驱结点,那我就使用有 前驱结点 的点进行删除,我们使用 当前节点的下一个结点的值 覆盖当前节点,那么相当于当前节点就已经被删除了,我们在处理下一个结点就可以了(下一个结点的前驱结点就是当前节点),问题就很容易解决了

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    void deleteNode(ListNode* node) {
        //由于是单向链表,只根据当前节点,无法获得 前驱结点,所以这里使用了覆盖的方法
            node->val=node->next->val;  //将后面一个节点的值覆盖当前节点
            node->next=node->next->next;    //删除后面的结点
        
    }
};

删除链表中重复的结点

acwing:删除重复的结点

题目:

在一个排序的链表中,存在重复的节点,请删除该链表中重复的节点,重复的节点不保留。

数据范围
链表中节点 val 值取值范围 [ 0 , 100 ] [0,100] [0,100]
链表长度 [ 0 , 100 ] [0,100] [0,100]

思路:

这里题目要求的是 不保留 重复的头结点,所以这里就使用一个暂时的结点,保存所有相同的结点,然后进行删除操作

而且本题在 牛客网 的《剑指offer》题单中没有出现

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 *     ListNode(int x) : val(x), next(NULL) {}
 * };
 */
class Solution {
public:
    ListNode* deleteDuplication(ListNode* head) {
        // ListNode* fast=head;
        // ListNode* slow=head;
        // while(fast!=nullptr)
        // {
        //     //保留重复节点,就是只删除多余的结点
        //     if(fast->val==slow->val)
        //     {
        //         while(fast->val==slow->val)
        //         {
        //             fast=fast->next;
        //         }
        //         slow->next=fast;
        //     }
        //     fast=fast->next;
        //     slow=slow->next;
        // }
        // return head;
        
        
        //可能会把头结点删掉,所以这里使用一个 虚拟头结点
        auto dummyHead=new ListNode(0);
        dummyHead->next=head;
        
        ListNode* cur=dummyHead;
        while(cur->next!=nullptr)
        {
            //记录相同的结点
            ListNode* tmp=cur->next;
            while(tmp!=nullptr&&cur->next->val==tmp->val)
            {
                tmp=tmp->next;
            }
            
            //只有一个节点
            if(cur->next->next==tmp)
            {
                cur=cur->next;
            }
            else
            {
                //有多个相同的结点,就将一整段 直接删掉
                cur->next=tmp;
            }
        }
        
        return dummyHead->next;;
    }
};

*正则表达式匹配

正则表达式匹配

LeetCode-10:正则表达式匹配

题目:

请实现一个函数用来匹配包括’.‘和’*'的正则表达式。

1.模式中的字符’.'表示任意一个字符

2.模式中的字符’*'表示它前面的字符可以出现任意次(包含0次)。

在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配

数据范围:

1.str 只包含从 a-z 的小写字母。

2.pattern 只包含从 a-z 的小写字母以及字符 . 和 ,无连续的 ''。

  1. 0 ≤ s t r . l e n g t h ≤ 26 0 \le str.length \le 26 0str.length26
  2. 0 ≤ p a t t e r n . l e n g t h ≤ 26 0 \le pattern.length \le 26 0pattern.length26

思路:

经典动态规划问题:
确定dp数组:dp[i][j]表示 s[i,...]p[j,...]均相匹配,就是si向后和 pj向后都是匹配的
初始化dp数组:f[n][m]=true, 表示初始时都是匹配的
确定递推公式:
p[j]是正常字段 f[i][j]=s[i]==p[j]&&f[i+1][j+1] ,就是 f[i][j]就只与 s[i]p[j]是否匹配有关了,并且以后的也都是匹配的
p[j]. f[i][j]=f[i+1][j+1],因为 .表示任何字符,所以必然匹配,只要后面的匹配就可以了
p[j+1]* f[i][j]=f[i][j+2]||f[i][j]=f[i+1][j], 因为 如果只匹配了 0 个字符,表示第j个字符不应该出现,就应该跳过jj+1 两个字符,与后面的匹配,如果包含其他多个字符,就只要满足和 s[i]的下一个字符匹配的长度就可以了,所以是s[i+1]

 class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param str string字符串 
     * @param pattern string字符串 
     * @return bool布尔型
     */
    
    int n,m;
    vector<vector<int>> dp;
    string s,p;
    
    bool match(string str, string pattern) {
        // write code here
        s=str;
        p=pattern;
        n=s.size();
        m=p.size();
        
        dp=vector<vector<int>> (n+1,vector<int>(m+1,-1));
        return Isdp(0,0);
        
        
    }
    
    bool Isdp(int x,int y)
    {
        if(dp[x][y]!=-1)
        {
            return dp[x][y];
        }
        //最后一个元素
        if(y==m)
        {
            if(x==n)
            {
                return true;
            }
            else
            {
                return false;
            }
            //return dp[x][y]=x==n;
        }
        
        bool first_match=x<n&&(p[y]=='.'||s[x]==p[y]);
        bool ans;
        if(y+1<m && p[y+1]=='*')
        {
            ans=Isdp(x, y+2) || first_match && Isdp(x+1,y);
        }
        else
        {
            ans=first_match&&Isdp(x+1,y+1);
        }
        
        return dp[x][y]=ans;
    }
};

很难搞,没有什么思路,代码也写不出来

时间复杂度分析: n n n 表示 s 的长度, m m m 表示 p 的长度,总共 n m nm nm 个状态,状态转移复杂度 O ( 1 ) O(1) O(1),所以总时间复杂度是 O ( n m ) O(nm) O(nm)

*表示数值的字符串

表示数值的字符串

题目:

请实现一个函数用来判断字符串str是否表示数值(包括科学计数法的数字,小数和整数)。

科学计数法的数字(按顺序)可以分成以下几个部分:

  1. 若干空格
  2. 一个整数或者小数
  3. (可选)一个 ‘e’ 或 ‘E’ ,后面跟着一个整数(可正可负)
  4. 若干空格

小数(按顺序)可以分成以下几个部分:

  1. 若干空格
  2. (可选)一个符号字符(‘+’ 或 ‘-’)
  3. 可能是以下描述格式之一:
    1. 至少一位数字,后面跟着一个点 ‘.’
    2. 至少一位数字,后面跟着一个点 ‘.’ ,后面再跟着至少一位数字
    3. 一个点 ‘.’ ,后面跟着至少一位数字
  4. 若干空格

整数(按顺序)可以分成以下几个部分:

  1. 若干空格
  2. (可选)一个符号字符(‘+’ 或 ‘-’)
  3. 至少一位数字
  4. 若干空格

思路:

按照要求 模拟所有可能的情况

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param str string字符串 
     * @return bool布尔型
     */
    bool isNumeric(string str) {
        //去除额外空格
        int i=0,j=str.size()-1;
        while(i<str.size()&&str[i]==' ')
        {
            i++;
        }
        while(j>=0&&str[j]==' ')
        {
            j--;
        }
        if(i>j)
        {
            //去除空格之后 为空,就直接返回 false
            return false;
        }
        
        //截取去除空格后的字符串
        str=str.substr(i,j-i+1);
        
        if(str[0]=='+'||str[0]=='-')
        {
            //截取掉 前面的符号位
            str=str.substr(1);
        }
        
        if(str.empty()||str[0]=='.'&& str.size()==1)
        {
            //如果为空,只有符号,这些都不满足 定义
//             if(str[0]>='0'&&str[0]<='9')
//             {
//                 return true;
//             }
            return false;
        }
        
        int dot=0,e=0;
        for(int i=0;i<str.size();i++)
        {
            if(str[i]>='0'&&str[i]<='9')
            {
                continue;
            }
            else if(str[i]=='.')
            {
                dot++;
                if(dot>1||e)
                {
                    //出现了多个点 或者在 点 的前面出现了e
                    return false;
                }
            }
            else if(str[i]=='e'||str[i]=='E')
            {
                e++;
                //e 的前面后面都要有数字
                if(!i || i+1==str.size()||e>1||str[i-1]=='.'&&i==1)
                {
                    return false;
                }
                if(str[i+1]=='+'||str[i+1]=='-')
                {
                    if(i+2==str.size())
                    {//在e后面的符号 是最后一位了
                        return false;
                    }
                    i++;
                }
                
            }
            else
            {
                return false;
            }
            
        }
        //其他可能的情况
        return true;
       
    }
}; 

调整数组顺序

调整数组顺序

题目:

输入一个长度为 n 整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前面部分,所有的偶数位于数组的后面部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变。

数据范围: 0 ≤ n ≤ 50000 0 \le n \le 50000 0n50000,数组中每个数的值 0 ≤ v a l ≤ 100000 0 \le val \le 100000 0val100000

要求:时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( n ) O(n) O(n)

进阶:时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( 1 ) O(1) O(1)

思路:

这里牛客网的有个约束条件, 要求保证 保证奇数和奇数,偶数和偶数之间的相对位置不变,所以就不能简单的考虑使用双指针思想了,这里使用的是 冒泡排序的思想,将 奇数 和 偶数 进行交换

这个条件在 《剑指offer》这本书中是没有的,所以 acwing上的 调整数组顺序 就没有这个约束条件,就可以直接使用 基于 快速排序思想的双指针做法,如下:

class Solution {
public:
bool isOdd(int num) 
{
  if (num % 2 == 1) 
  {
      return true;
  }
  return false;
}
bool isEnev(int num) 
{
  if (num % 2 == 0) 
  {
      return true;
  }
  return false;
}

void reOrderArray(vector<int> &array) 
{
  if(array.size()==0)
  {
      return;
  }

  //基于快速排序思想
  int left=-1,right=array.size();

  while(left<right)
  {
      do left++ ;
          while(isOdd(array[left]));
      do right--;
          while(isEnev(array[right]));

      if(left<right)
      {
          swap(array[left],array[right]);
      }
  }


}
};

还可以写的更加简略:

class Solution {
public:
 void reOrderArray(vector<int> &array) {
      int l = 0, r = array.size() - 1;
      while (l < r) {
          while (l < r && array[l] % 2 == 1) l ++ ;
          while (l < r && array[r] % 2 == 0) r -- ;
          if (l < r) swap(array[l], array[r]);
      }
 }
};

当两个指针相遇时,走过的总路程长度是 n n n,所以时间复杂度是 O ( n ) O(n) O(n)

 class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param array int整型vector 
     * @return int整型vector
     */
    vector<int> reOrderArray(vector<int>& array) {
        // write code here
        int size=array.size();
        //基于 冒泡排序的思想
        for(int i=0;i<size-1;i++)
        {
            for(int j=0;j<size-1-i;j++)
            {
                //奇数 偶数 进行交换
                if(array[j]%2==0&&array[j+1]%2!=0)
                {
                    swap(array[j],array[j+1]);
                }
            }
        }
        return array;
    }
};

时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( 1 ) O(1) O(1)

链表中倒数第k个节点

链表中倒数第k个节点

题目:

输入一个长度为 n 的链表,设链表中的元素的值为 ai ,返回该链表中倒数第k个节点。

如果该链表长度小于k,请返回一个长度为 0 的链表。

数据范围: 0 ≤ n ≤ 1 0 5 0 \leq n \leq 10^5 0n105,$0 \leq a_i \leq 10^9, 0 ≤ k ≤ 1 0 9 0 \leq k \leq 10^9 0k109

要求:空间复杂度 O ( n ) O(n) O(n),时间复杂度 O ( n ) O(n) O(n)

进阶:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)

思路:

比较容易实现,使用双指针法,先让 快指针 走 k步,再让 快慢指针 同时走,当快指针到头时,这时慢指针所指的就是倒数第k个了

 /**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 *	ListNode(int x) : val(x), next(nullptr) {}
 * };
 */
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param pHead ListNode类 
     * @param k int整型 
     * @return ListNode类
     */
    ListNode* FindKthToTail(ListNode* pHead, int k) {
        // write code here
        //防止空链表
        if(pHead==nullptr)
        {
            return nullptr;
        }
        ListNode* fast=pHead;
        ListNode* slow=pHead;
        
        //快指针先走k步
        while(k--)
        {
            //如果链表长度小于k,那么fast指针就越界了
            if(fast==nullptr)
            {
                return nullptr;
            }
            fast=fast->next;
        }
        
        //快慢指针 再同时走
        while(fast!=nullptr)
        {
           
            slow=slow->next;
            fast=fast->next;
        }
        //返回慢指针
        return slow;
    }
};

时间复杂度 O ( N ) O(N) O(N):N为链表长度,遍历整个链表

空间复杂度 O ( 1 ) O(1) O(1):使用额外常数大小空间

链表的环

链表的环

题目:

给一个长度为n链表,若其中包含环,请找出该链表的环的入口结点,否则,返回null。

数据范围: n ≤ 10000 n\le10000 n10000 1 < = 结 点 值 < = 10000 1<=结点值<=10000 1<=<=10000

要求:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)

 /*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* EntryNodeOfLoop(ListNode* pHead) {
        ListNode* fast=pHead;
        ListNode* slow=pHead;
        
        while(fast!=nullptr&&fast->next!=nullptr)   //fast->next!=nullptr可以防止只有一个节点的情况
        {
            //如果快慢指针相遇,则一定是在环内相遇的
            slow=slow->next;
            fast=fast->next->next;

            //如果快慢指针相遇,就说明有环,就要找 环的入口
            if(fast==slow)
            {
                //根据公式可以推倒
                ListNode* index1=fast;  //相遇处
                ListNode* index2=pHead;  //链表头
                while(index1!=index2)
                {
                    index1=index1->next;
                    index2=index2->next;
                }
                return index2;  //返回环的入口
            }
        }
        return nullptr;
    }
};

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

反转链表

反转链表

题目:

给定一个单链表的头结点pHead(该头节点是有值的,比如在下图,它的val是1),长度为n,反转该链表后,返回新链表的表头。

数据范围: 0 ≤ n ≤ 10000 0\leq n\leq10000 0n10000

要求:空间复杂度 O ( 1 ) O(1) O(1) ,时间复杂度 O ( n ) O(n) O(n)

思路:

反转链表可以说是在链表中 出场率很高的题目了,一定要熟练掌握

 /*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        ListNode* pre=nullptr;
        ListNode* cur=pHead;
        ListNode* tmp=nullptr;
        while(cur!=nullptr)
        {
            //保存下一个结点
            tmp=cur->next;
            
            //重复这个过程
            cur->next=pre;
            //原地改变指针指向
            pre=cur;
            cur=tmp;
            
        }
        return pre;
    }
};

时间复杂度: O ( n ) O(n) O(n), 遍历一次链表
空间复杂度: O ( 1 ) O(1) O(1)

合并链表

合并链表

题目:

输入两个递增的链表,单个链表的长度为n,合并这两个链表并使新链表中的节点仍然是递增排序的。

数据范围: 0 ≤ n ≤ 10000 0 \le n \le 10000 0n10000 − 1000 ≤ 节 点 值 ≤ 1000 -1000 \le 节点值 \le 1000 10001000
要求:空间复杂度 O ( 1 ) O(1) O(1),时间复杂度 O ( n ) O(n) O(n)

归并排序的思想,由于两个链表是有序的,所以可以直接开辟一个新的目标链表,每次放入这两个链表中较小的一个。最后如果某个链表有剩余,就直接将其放入。

 /*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* Merge(ListNode* pHead1, ListNode* pHead2) {
        //使用虚拟头结点
        ListNode* dummyHead=new ListNode(0);
        ListNode* cur=dummyHead;
        //归并排序的思想
        while(pHead1!=nullptr&&pHead2!=nullptr)
        {
            //放入小的结点
            if(pHead1->val<=pHead2->val)
            {
                cur->next=pHead1;
                pHead1=pHead1->next;
            }
            else
            {
                cur->next=pHead2;
                pHead2=pHead2->next;
            }
            cur=cur->next;
        }
        
        //将剩余的未放入的结点继续放入
        if(pHead1!=nullptr)
        {
            cur->next=pHead1;
            pHead1=pHead1->next;
        }
        if(pHead2!=nullptr)
        {
            cur->next=pHead2;
            pHead2=pHead2->next;
        }
       //记得不要反悔虚拟头结点
        return dummyHead->next;
    }
};

两个链表各遍历一次,所以时间复杂度为 O ( n ) O(n) O(n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值