【《剑指offer》系列刷题笔记 ---- 1~5题】

1、JZ3 数组中重复的数字

描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任一一个重复的数字。 例如,如果输入长度为7的数组[2,3,1,0,2,5,3],那么对应的输出是2或者3。存在不合法的输入的话输出-1

示例1
输入:[2,3,1,0,2,5,3]
返回值:2

方法1:

1.先利用sort算法对容器执行从小到大的排序

2.从头开始比较numbers[i]和numbers[i+1]是否相等

如果相等则返回numbers[i]

遍历完没有找到返回 -1

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param numbers int整型vector 
     * @return int整型
     */
    int duplicate(vector<int>& numbers) 
    {
        // write code here
        sort(numbers.begin(),numbers.end());
        for(int i =1;i<numbers.size();i++)
        {
            if(numbers[i-1]==numbers[i])
            {
                return numbers[i];
            }
         
          }
        return -1;
    }
};

方法2:

题解 | #数组中重复的数字#

题目的主要信息:
  • 一个长度为n*的数组中只有 0 到 n−1的数字
  • 需要找出其中任意一个重复出现的数字
举一反三:

学习完本题的思路你可以解决如下题目:

JZ56. 数组中只出现一次的两个数字

JZ50. 第一个只出现一次的字符

JZ75. 字符流中第一个不重复的字符

方法一:位置重排(推荐使用)

思路:

既然数组长度为nnn只包含了0到n−1n-1n−1的数字,那么如果数字没有重复,这些数字排序后将会与其下标一一对应。那我们就可以考虑遍历数组,每次检查数字与下标是不是一致的,一致的说明它在属于它的位置上,不一致我们就将其交换到该数字作为下标的位置上,如果交换过程中,那个位置已经出现了等于它下标的数字,那肯定就重复了。

具体做法:

  • step 1:遍历数组,遇到数组元素与下标相同的不用管。
  • step 2:遇到数组元素与下标不同,就将其交换到属于它的位置,交换前检查那个位置是否有相同的元素,若有则重复。
  • step 3:遍历结束完全交换也没重复,则返回-1.
class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param numbers int整型vector 
     * @return int整型
     */
    
    //方法2:位置重排
     int duplicate(vector<int>& numbers) 
    {   

        for(int i=0;i<numbers.size();i++)
        {
           
            if(i == numbers[i]) //如果i在自己对应的位置上不用管
            {
                continue;
            }
            else
            {
                if(numbers[i] == numbers[numbers[i]]) //如果两者相等说明找到相同元素
                {
                    return numbers[i];
                }
                else
                {    //如果两者不相等,交换一下位置
                    swap(numbers[i],numbers[numbers[i]]);
                }
            }
        }
         return -1;
    }
};

2、JZ4 二维数组中的查找

描述

在一个二维数组array中(每个一维数组的长度相同),每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

[

[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]

]

给定 target = 7,返回 true。

给定 target = 3,返回 false。

题目的主要信息:
  • 矩阵的行元素和列元素都是有序的,从左到右递增,从上到下递增,完全递增元素不会有重复
  • 找到矩阵中有没有给定元素即可
举一反三:

学习完本题的思路你可以解决如下题目:

BM17.二分查找-I

BM19.寻找峰值

BM21.旋转数组

方法:二分查找(推荐使用)

知识点:分治

分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。

首先看四个角,左上与右下必定为最小值与最大值,而左下与右上就有规律了:左下元素大于它上方的元素,小于它右方的元素,右上元素与之相反。既然左下角元素有这么一种规律,相当于将要查找的部分分成了一个大区间和小区间,每次与左下角元素比较,我们就知道目标值应该在哪部分中,于是可以利用分治思维来做。

//两层循环,遍历二维数组
for(int i = 0; i < n; i++)  
    for(int j = 0; j < m; j++)
        //找到target
        if(array[i][j] == target)  
            return true;

具体做法:

  • step 1:首先获取矩阵的两个边长,判断特殊情况。
  • step 2:首先以左下角为起点,若是它小于目标元素,则往右移动去找大的,若是他大于目标元素,则往上移动去找小的。
  • step 3:若是移动到了矩阵边界也没找到,说明矩阵中不存在目标值。

实现代码:

class Solution {
public:
    bool Find(int target, vector<vector<int> > array) {
        //特殊判断:如果数组为空或第一行为空返回FALSE
        if(array.size() == 0||array[0].size() ==0)
            return false;
        //获取数组行数
        int i=array.size()-1;
        int j=0;
        while(i>=0&&j<array[0].size())
        {
            if(array[i][j]==target)
            {
                return true;
            }
            else
            {
                //如果当前值大于目标值往上移动
                if(array[i][j]>target)   
                {
                    i--;
                }
                else
                {
                    //如果当前值小于目标值往上右移动
                    j++;
                }
            }
        }
        return false;
    }
};

描述

请实现一个函数,将一个字符串s中的每个空格替换成“%20”。

例如,当字符串为We Are Happy.则经过替换之后的字符串为We%20Are%20Happy。

数据范围:0 ≤len(s)≤1000 。保证字符串中的字符为大写英文字母、小写英文字母和空格中的一种。

题目的主要信息:
  • 将一个字符串s中的每个空格替换成“%20”
  • 保证字符串中的字符为大写英文字母、小写英文字母和空格中的一种
举一反三:

学习完本题的思路你可以解决如下题目:

JZ73. 翻转单词序列

方法:字符串截取相加(推荐使用)

具体做法:

  • 我们可以用下标遍历字符串,每次检查下标所在位置的字符是否为空格,如果不是空格,下标继续往后

  • 如果是空格则调用substr函数将字符串从空格前后截断,然后中间添加"%20"后相连即可。

实现代码:

class Solution {
public:
    /**
     * 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
     *
     * 
     * @param s string字符串 
     * @return string字符串
     */
    string replaceSpace(string s) {
        // write code here
        string ret="";
        for(int i=0;i<s.length();i++)
        {
            //如果没有遇到空格,继续
            if(s[i] != ' ')
            {
                ret+=s[i];
            }
            else//遇到空格将空格替换为%20
            {
                ret+= "%20";
            }
            
        }
        
        return ret;
    }
};

3、JZ6 从尾到头打印链表

描述

输入一个链表的头节点,按链表从尾到头的顺序返回每个节点的值(用数组返回)。

如输入{1,2,3}的链表如下图:

在这里插入图片描述

返回一个数组为[3,2,1]

0 <= 链表长度 <= 10000

示例1

输入:

{1,2,3}

复制

返回值:

[3,2,1]

复制

示例2

输入:

{67,0,24,58}

复制

返回值:

[58,24,0,67
方法一:递归(推荐使用)

知识点:递归

递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。

思路:

我们都知道链表无法逆序访问,那肯定无法直接遍历链表得到从尾到头的逆序结果。但是我们都知道递归是到达底层后才会往上回溯,因此我们可以考虑递归遍历链表,因此三段式如下:

  • 终止条件: 递归进入链表尾,即节点为空节点时结束递归。
  • 返回值: 每次返回子问题之后的全部输出。
  • 本级任务: 每级子任务递归地进入下一级,等下一级的子问题输出数组返回时,将自己的节点值添加在数组末尾。

具体做法:

  • step 1:从表头开始往后递归进入每一个节点。
  • step 2:遇到尾节点后开始返回,每次返回依次添加一个值进入输出数组。
  • step 3:直到递归返回表头。

实现代码:

/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:
    //递归行数
    void recursion(ListNode* head,vector<int>&res)
    {
        if(head != NULL)
        {//先遍历到最后一个节点
            recursion(head->next, res);
            //把链表从头到尾填充进数组
            res.push_back(head->val);
        }
    

        
    }
    vector<int> printListFromTailToHead(ListNode* head) {
        
        vector<int>res;
        recursion(head, res);
        return res;
        
    }
};
方法二:栈(扩展思路)

知识点:栈

栈是一种仅支持在表尾进行插入和删除操作的线性表,这一端被称为栈顶,另一端被称为栈底。元素入栈指的是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;元素出栈指的是从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

思路:

递归的思想也可以用栈实现,因为栈是先进后出的,符合逆序的特点,递归本质上就是用栈实现的。

具体做法:

  • step 1:我们可以顺序遍历链表,将链表的值push到栈中。
  • step 2:然后再依次弹出栈中的元素,加入到数组中,即可实现链表逆序。

实现代码:

/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:
    //方法2:使用栈实现
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int>res;
        stack<int>temp;
        ListNode* p=head;
        while(p !=NULL)
        {
            //入栈
            temp.push(p->val);
            p=p->next;
        }
        int len=temp.size();
//         for(int i=0;i<temp.size();i++) //随着出栈,栈的长度发生改变
        for(int i=0;i<len;i++)
        {
            //将栈顶元素放入数组中
            res.push_back(temp.top());
            //出栈
            temp.pop();
        }
        
        return res;
    }
    
};

方法3: 利用vector的逆序输出

/**
*  struct ListNode {
*        int val;
*        struct ListNode *next;
*        ListNode(int x) :
*              val(x), next(NULL) {
*        }
*  };
*/
class Solution {
public:

    
    //方法3:使用逆序输出
    vector<int> printListFromTailToHead(ListNode* head) {
        vector<int> res;
        ListNode* p = head;
        while(p !=NULL)
        {
            res.push_back(p->val);
            p = p->next;
        }
        return vector<int>(res.rbegin(),res.rend());  //逆序返回
    }
   
    
};

4、BM1 反转链表

描述

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

数据范围: 0≤n≤1000

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

如当输入链表{1,2,3}时,

经反转后,原链表变为{3,2,1},所以对应的输出为{3,2,1}。

以上转换过程如下图所示:

在这里插入图片描述

示例1

输入:

{1,2,3}

复制

返回值:

{3,2,1}

复制

示例2

输入:

{}

复制

返回值:

{}

复制

说明:

空链表则输出空        

解决思路:

1,使用栈解决

链表的反转是老生常谈的一个问题了,同时也是面试中常考的一道题。最简单的一种方式就是使用栈,因为栈是先进后出的。实现原理就是把链表节点一个个入栈,当全部入栈完之后再一个个出栈,出栈的时候在把出栈的结点串成一个新的链表。原理如下
在这里插入图片描述

代码实现:

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:
    ListNode* ReverseList(ListNode* pHead) {
        //用一个指针指向头结点

    ListNode* p = pHead;
        stack<int>stk;
        while(p != NULL)
        {
            stk.push(p->val);
            p=p->next;
        } 
        int len = stk.size();
        //指针重新指向pHead
        p=pHead;
        //把栈内数据赋值给链表
        for(int i =0;i<len;i++)
        {
            p->val=stk.top(); //取出栈顶元素
            p=p->next;   //指向下一个结点
            stk.pop();   //出栈
        }
        
        return pHead;
    }
};

解法二:迭代

  • 在遍历链表时,将当前节点的next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。

代码实现:

/*
struct ListNode {
	int val;
	struct ListNode *next;
	ListNode(int x) :
			val(x), next(NULL) {
	}
};*/
class Solution {
public:

       ListNode* ReverseList(ListNode* pHead) {
           //pre指针:用来指向反转后的节点,初始化为null
           ListNode* pre = NULL;
           //当前节点指针
           ListNode* cur = pHead;
           while(cur != NULL)
           {
               //cur_next 节点,永远指向当前cur的下一个结点
               ListNode* cur_next = cur->next;
               //反转的关键:当前节点指向前一个节点
               cur->next = pre;
               pre = cur;
               cur = cur_next;
           }
           //pre是反转后的头节点
           return pre;
       }
};

5、BM2 链表内指定区间反转

描述

将一个节点数为 size 链表 m 位置到 n 位置之间的区间反转,要求时间复杂度 O(n)O(n),空间复杂度 O(1)O(1)。
例如:
给出的链表为 1→2→3→4→5→NULL, m=2,n=4
返回 1→4→3→2→5→NULL

数据范围: 链表长度 0 < s10000<size≤1000,0 < 0<mnsize,链表中每个节点的值满足∣val∣≤1000

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

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

示例1

输入:

{1,2,3,4,5},2,4

复制

返回值:

{1,4,3,2,5}

复制

示例2

输入:

{5},1,1

复制

返回值:

{5}
方法一:头插法迭代(推荐使用)

思路:

在学会了BM1.反转链表之后,要解决这个问题就很简单了,前一题是整个链表反转,这一题是部分反转,这上一题就是这道题的前置问题啊。那我们肯定是要先找到了第m个位置才能开始反转链表,而反转的部分就是从第m个位置到第n个位置,反转这部分的时候就参照BM1.反转链表

while(cur != null){
    //断开链表,要记录后续一个
    ListNode temp = cur.next; 
    //当前的next指向前一个
    cur.next = pre; 
    //前一个更新为当前
    pre = cur; 
    //当前更新为刚刚记录的后一个
    cur = temp; 
}

具体做法:

  • step 1:我们可以在链表前加一个表头,后续返回时去掉就好了,因为如果要从链表头的位置开始反转,在多了一个表头的情况下就能保证第一个节点永远不会反转,不会到后面去。
  • step 2:使用两个指针,一个指向当前节点,一个指向前序节点。
  • step 3:依次遍历链表,到第m个的位置。
  • step 4:对于从m到n这些个位置的节点,依次断掉指向后续的指针,反转指针方向。
  • step 5:返回时去掉我们添加的表头。
/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */

class Solution {
public:
    /**
     * 
     * @param head ListNode类 
     * @param m int整型 
     * @param n int整型 
     * @return ListNode类
     */
    ListNode* reverseBetween(ListNode* head, int m, int n) {
        // write code here
       /* ListNode* p = head;
        ListNode* p_m =NULL;
        ListNode* p_n =NULL;
        
        //遍历找到m和n节点
        for(int i = 1;p != NULL;i++)
        {
            //用p_m指向第m个节点
            if(i==m)
            {
                p_m = p; 
            }
            //用p_n指向第n个节点    
            if(i == n)
            {
                 p_n = p; 
            }
            p=p->next;
            if(p_m != NULL && p_n != NULL)
                break;
        }
        //交换两个节点的数据
        int temp = p_m->val;
        p_m->val = p_n->val;
        p_n->val = temp;
        return head;*/
        //加个表头
        ListNode* res = new ListNode(-1);
        res->next = head;
        //前序节点
        ListNode* pre = res;
        //当前节点
        ListNode* cur = head;
        //找到m
        for(int i =1;i<m;i++)
        {
            pre = cur;
            cur = cur->next;
        }
        //从m反转到n
        for(int i = m;i<n;i++)
        {
            ListNode* temp = cur->next;
            cur->next = temp->next;
            temp->next = pre->next;
            pre->next = temp;
        }
        return res->next;
    }
    
};

**方法二:栈 **

解题思路:

  • 判断参数head是否为空,若空则直接返回;判断m是否等于n,相等则无需进行操作,直接返回;

  • 由于题目给出的链表是不带头结点的,所以令i=1,work=head,首先用work指针,从链表头部开始进行遍历,遍历前m-1元素,最后一次遍历时,work指向第m个元素,同时以start指针记录work的当前位置以便下一次遍历时能够直接从第m个元素开始;

  • 顺序遍历第m到第n位置的元素,并用栈进行存储;

  • 再次从start出开始进行顺序遍历直到栈空,每遍历一个新结点时将栈顶元素赋值给当前结点,实现反转链表。

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

编写代码:

/**
 * struct ListNode {
 *	int val;
 *	struct ListNode *next;
 * };
 */

class Solution {
public:
    /**
     * 
     * @param head ListNode类 
     * @param m int整型 
     * @param n int整型 
     * @return ListNode类
     */
    ListNode* reverseBetween(ListNode* head, int m, int n) {
        // write code here
       ListNode* p = head;
        ListNode* p_m =NULL;
        ListNode* p_n =NULL;
        
        //遍历找到m和n节点
        int i = 1;
        for(;i<m;i++)
        {
            //用p_m指向第m个节点
            p=p->next;
        }
        //start 指向刚开始p_m
        p_m = p;
        stack<int>stk;
        while(i<=n)
        {
            //cur_next节点,永远指向当前cur的下一个节点
            stk.push(p->val);
            p=p->next;
            i++;
        }
        p = p_m;
        while(!stk.empty())
        {
            p->val = stk.top();
            stk.pop();
            p = p->next;
        }
        return head;
       
    }
    
};
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值