《剑指offer》题目汇总

文章目录

      • 面试题1 赋值运算符函数
        • 以上是基本的内容,但还是有代码异常安全性问题存在。
      • 面试题3 找出数组中的重复数字
        • 借助哈希表【这里用数组代替】,时间复杂度为 $O(n)$ ,引入 $O(n)$ 空间复杂度
        • 时间复杂度为 $O(n)$ ,空间复杂度为 $O(1)$
        • 不改变原来的数组,时间复杂度为 $O(nlog(n))$ ,同时空间复杂度为 $O(1)$
      • 面试题18 删除链表中重复的节点
      • 面试题 43 1-n整数中1出现的次数
        • 不考虑时间效率
        • 牛客商有意思的解法
        • 从数学规律入手,明显提高时间效率

面试题1 赋值运算符函数

题目:如下为类型CMyString的声明,请为该类型添加赋值运算符函数。

class CMyString
{
public:
	CMyString(char* pData = nullptr);
	CMyString(const CMyString& str);
	~CMyString(void);
private:
	char* m_pData;
};

考察基本知识需要注意的几个问题:

  • 是否把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(*this)。因为返回该类型的引用才允许连续赋值,否则不可以。假设有3个CMyString的对象:str1,str2,str3,返回引用允许str1=str2=str3;
  • 是否把传入的参数的类型声明为常量引用。如果传入的参数不是对象的引用,而是对象的实例,那么从形参到实参会调用一次复制构造函数,增加无谓的消耗,降低代码的效率。同时,赋值的过程中不会改变实例的状态,所以引用参数加上const关键字。
  • 是否释放实例自身已有的内存。如果没有会造成内存泄漏。
  • 判断传入的参数和当前实例(*this)是不是同一个实例。如果是,则不进行赋值操作,直接返回。如果不判断就进行赋值,那么释放自身已有的内存的时候会导致参数的内训也被释放,找不到需要赋值的内容。
CMyString & CMyString::operator==(const CMyString &str)
{
	if(this == &str)
		return *this;
	delete []m_pData;
	m_pData = nullptr;
	m_pData = new char(strlen(str.m_pData)+1);
	strcpy(m_pData ,str.m_pData);
	return *this;
}

以上是基本的内容,但还是有代码异常安全性问题存在。

  在分配内存之前先用delete释放了实例m_pData的内存,如果此时内存不足导致new char抛出异常,则m_pData将是一个空指针,这样非常容易引起程序崩溃。违背了异常安全性准则。
  在赋值运算符函数中实现异常安全性有两种方法:

  • 简单方法,先用new分配新内存,如果分配失败就不会delete释放已有的内存,这样能确保如果内存分配失败则不会改变原来实例的内容。
CMyString & CMyString::operator==(const CMyString &str)
{
	if(this == &str)
		return *this;
	m_pData = new char(strlen(str.m_pData)+1);
	delete []m_pData;
	m_pData = nullptr;	
	strcpy(m_pData ,str.m_pData);
	return *this;
}
  • 更好的方法:先创建一个临时实例,再交换临时实例和原来实例。
CMyString & CMyString ::operator==(const CMyString &str)
{
	if(this ! = &str)
	{
		CMyString  temp = CMyString(str);
		char* pTemp = temp.m_pData;
		temp.m_pData = m_pData;
		m_pData = pTemp ;
	}
	return *this;
}

  这时由于temp是一个局部变量,当运行到if结束就会自动调用析构函数释放temp.m_pData内存,而temp.m_pData指向原来m_pData,也就是释放了原来实例的内存。

面试题3 找出数组中的重复数字

在一个长度为n的数组里的所有数字都在0到n-1的范围内。 数组中某些数字是重复的,但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。

借助哈希表【这里用数组代替】,时间复杂度为 O ( n ) O(n) O(n) ,引入 O ( n ) O(n) O(n) 空间复杂度

思路:按照剑指offer提供的思路,首先,可以对数组先进行排序,然后就可以很顺利找到重复数字了。【时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),涉及到排序算法,暂时没有实现】。另外可以利用哈希表查找迅速的特点,建立一个哈希表,因为数组可以作为哈希表特殊情况处理,下标作为key,数值作为value,所以首先根据这种思路设计以下方案。时间复杂度为 O ( n ) O(n) O(n),但是因为存储哈希数组需要空间,所以额外引入了 O ( n ) O(n) O(n)空间复杂度。

class Solution {
public:
    // Parameters:
    //        numbers:     an array of integers
    //        length:      the length of array numbers
    //        duplication: (Output) the duplicated number in the array number
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    
    bool duplicate(int numbers[], int length, int* duplication) {
        if(numbers == nullptr || length <= 0)
            return false;
        for(int i=0;i<length;i++)
        {
            if(numbers[i]<0 || numbers[i] > length-1)
                return false;
        }
        int temp[20];
        memset(temp,-1,sizeof(temp));
        for(int i=0;i<length;++i)
        {
            if(temp[numbers[i]] == -1)
            {
                temp[numbers[i]] = numbers[i];
            }else{
                *duplication = numbers[i];
                return true;
            }
        }
        return false;
    }
};

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

思路:因此希望找到空间复杂度为 O ( 1 ) O(1) O(1)的方法。题目中数字都在0~ n − 1 n-1 n1范围内,如果数组中没有重复数字,那么当数组排序之后数字 i i i将出现在下标为 i i i位置。但是因为数组中有重复数字,所以有可能有的位置将出现多个数字,但是有的位置没有数字。
  利用这个特点,对整个数组进行重排。从头到尾依次扫描整个数组的值,当扫描到第 i i i个位置时,如果该位置的值 m m m不等于i,就将这个数字与 m m m位置的数字进行比较,如果相等就说明有数字出现了重复,因为位置 i i i,和位置 m m m的数字相同,如果不相等就将两个数字交换,使值为 m m m的数放到他应该在位置上,如果等于 i i i就继续扫描下一个位置。接下来重复这个过程。
  下面以一个具体的例子来说明整个方法,该方法的看似两层循环,实际上每个数字最多只需要交换两次就能找到他的位置,因此总的时间复杂度为 O ( n ) O(n) O(n),没有引入空间复杂度。《剑指offer》题目汇总_第1张图片

class Solution {
public:
    // Parameters:
    //        numbers:     an array of integers
    //        length:      the length of array numbers
    //        duplication: (Output) the duplicated number in the array number
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    
    bool duplicate(int numbers[], int length, int* duplication) {
        if(numbers == nullptr || length <=0)
            return false;
        for(int i=0;i<length;i++)
        {
            if(numbers[i]<0 || numbers[i]>length-1)
                return false;
        }
        for(int i=0;i<length;i++)
        {
            while(numbers[i] != i)
            {
                if(numbers[i] == numbers[numbers[i]])
                {
                    *duplication = numbers[i];
                     return true;
                }
                int temp = numbers[i];
                numbers[i] = numbers[temp];
                numbers[temp] = temp;
            }
        }
        return false;
    }
};

不改变原来的数组,时间复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n)) ,同时空间复杂度为 O ( 1 ) O(1) O(1)

思路:上面的解法虽然没有引入空间复杂度,但是直接遍历数组对数字进行排序,改变了原来的数组,有没有一种方法,不改变数组也不引入空间复杂度。
  根据题目来说,长度为n的数组数字都在0-n-1之间,如果在数组的某一个区间内,例如数字0-n/2之间数字的总个数超过了n/2+1,那么我们可以判定这个区间内一定有重复数字。根据这个特点,我们将数组根据中间值一直二分直到找到重复的数字。以下图为例说明算法原理:
《剑指offer》题目汇总_第2张图片
begin从1开始,与上述题目不太一样,要求数组中数字是正整数

class Solution {
public:
    // Parameters:
    //        numbers:     an array of integers
    //        length:      the length of array numbers
    //        duplication: (Output) the duplicated number in the array number
    // Return value:       true if the input is valid, and there are some duplications in the array number
    //                     otherwise false
    bool duplicate(int numbers[], int length, int* duplication) {
        if(numbers == nullptr || length <= 0)
            return false;
        for(int i=0;i<length;i++)
        {
            if(numbers[i]<0 || numbers[i]>length -1)
                return false;
        }
        int begin = 1;
        int end = length-1;
        while(end >= begin)
        {
            int middle = ((end-begin)>>1) + begin;
            int count  = countRange(numbers,length,begin,middle);
            if(end == begin)
            {
                if(count>1)
                {
                    *duplication = begin;
                    return true;
                }
                else
                    break;
            }
            if(count>(middle-begin+1))
                end = middle;
            else
                begin = middle+1;
        }
        return false;
    }
    int countRange(const int * numbers,int length,int begin,int end)
    {
        if(numbers == nullptr)
            return 0;
        int count=0;
        for(int i=0;i<length;i++)
        {
            if(numbers[i]>=begin && numbers[i]<=end)
                count++;
        }
        return count;
    }
};

面试题18 删除链表中重复的节点

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead)
    {
        if (pHead == nullptr)
            return nullptr;
        if (pHead != nullptr && pHead->next == nullptr)
            return pHead;
                 
        ListNode* current;
        if ( pHead->next->val == pHead->val){
            current = pHead->next->next;
            while (current != nullptr && current->val == pHead->val)
                current = current->next;
            return deleteDuplication(current);                     
        }
        else {
            current = pHead->next;
            pHead->next = deleteDuplication(current);
            return pHead;
        }    
    }
};

非递归方法,使用三个指针指示链表相邻数据之间的关系,为了防止头指针被删除,剑指offer中将形参设置为指向头指针的指针,这样可以即使头指针被删除仍然能找到其地址进行数值移动,但是在线编程给定了函数接口,形参就是头指针,如果头指针被删除,就找不到开始指针的地址了。参考答案,找到了该问题的解决方法,新建一个节点,防止头指针消失。ListNode* newHead=new ListNode(-1); newHead->next=pHead; 虽然是一个小问题,但是解决掉还是觉得很惊喜。

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) :
        val(x), next(NULL) {
    }
};
*/
class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead)
    {
        if(pHead == nullptr)
            return nullptr;
        if(pHead != nullptr && pHead->next == nullptr)
            return pHead;
        
        ListNode* newHead=new ListNode(-1);
        newHead->next=pHead;
        ListNode* preNode = newHead;
        ListNode* pNode = pHead;
        while(pNode != nullptr)
        {
            ListNode* pNext = pNode->next;
            bool deleteNode = false;
            if(pNext != nullptr && pNode->val == pNext->val)
                deleteNode = true;
            if(!deleteNode)
            {
                preNode = pNode;
                pNode = pNode->next;
            }else{
                int value = pNode->val;
                ListNode* delNode = pNode;
                while(delNode != nullptr && delNode->val == value)
                {
                    pNext = delNode->next;
                    delete delNode;
                    delNode = nullptr;
                    delNode = pNext;
                }
                preNode->next = pNext;
                pNode = pNext;
            }
        }
        return newHead->next;
    }
};

面试题 43 1-n整数中1出现的次数

题目:输入一个整数 n n n,求 1 − n 1-n 1n n n n个整数的十进制表示中1出现的次数,例如,输入12, 1 − 12 1-12 112这些整数中包含1的数字有1,10,11,12,一共出现了5次。

不考虑时间效率

最直观的方法,就是累加1-n中每个整数1出现的次数。可以每次通过对10求余数判断整数的个位数字是不是1如果这个数字大于10,则除以10之后再判断个位数字是不是1. 如果输入数字是n,根据2进制计算知道这个数一共有 l o g n logn logn位,需要判断每一位是不是1,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。计算量还是比较大的。

class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n)
    {
        if(n <= 0 )
            return 0;
        int count = 0;
        for(int i=1 ;i<= n;i++)
        {
            count = count + Count(i);
        }
        return count;
    }
    int Count(int n)
    {
        int count =0;
        while(n)
        {
            if(n%10 ==1)
                count++;
            n=n/10;
        }
        return count;
    }
    
};

牛客商有意思的解法

这种解法的时间复杂度应该是 O ( l o g n ) O(logn) O(logn),很简单,思路清晰。
主要思路:设定整数点(如1、10、100等等)作为位置点 i i i(对应 n n n的各位、十位、百位等等),分别对每个数位上有多少包含1的点进行分析
  根据设定的整数位置,对 n n n进行分割,分为两部分,高位 n / i n/i n/i,低位 n % i n\%i n%i

  1. i i i表示百位,且百位对应的数 > = 2 >=2 >=2,如 n = 31456 n=31456 n=31456, i = 100 i=100 i=100,则 a = 314 a=314 a=314, b = 56 b=56 b=56,此时百位为1的次数有 a / 10 + 1 = 32 a/10+1=32 a/10+1=32(最高两位0~31),每一次都包含100个连续的点,即共有 ( a % 10 + 1 ) ∗ 100 (a\%10+1)*100 (a%10+1)100个点的百位为1。因为大于1,所以最高位为31时,后面也能满足有100个百位为1 的数。
  2. i i i表示百位,且百位对应的数为1,如 n = 31156 n=31156 n=31156, i = 100 i=100 i=100,则 a = 311 a=311 a=311, b = 56 b=56 b=56,此时百位对应的就是1,则共有 a a%10 a(最高两位0-30)次是包含100个连续点,当最高两位为31(即a=311),本次只对应局部点00~56,共 b + 1 b+1 b+1次,所有点加起来共有 ( a % 10 ∗ 100 ) + ( b + 1 ) (a\%10*100)+(b+1) a%10100+(b+1),这些点百位对应为1。因为百位就为1,所以当前面最高位为最大值31时,后面只能有百位为1的100-156共57个值,也就是 b + 1 b+1 b+1
  3. i i i表示百位,且百位对应的数为0,如 n = 31056 n=31056 n=31056, i = 100 i=100 i=100,则 a = 310 a=310 a=310, b = 56 b=56 b=56,此时百位为1的次数有 a / 10 = 31 a/10=31 a/10=31(最高两位0~30)
      综合以上三种情况,当百位对应0或 > = 2 >=2 >=2时,有 ( a + 8 ) / 10 (a+8)/10 (a+8)/10次包含所有100个点,还有当百位为1( a a%10==1 a),需要增加局部点 b + 1 b+1 b+1。之所以补8,是因为当百位为0,则 a / 10 = = ( a + 8 ) / 10 a/10==(a+8)/10 a/10==(a+8)/10,当百位 > = 2 >=2 >=2,补8会产生进位位,效果等同于 ( a / 10 + 1 ) (a/10+1) (a/10+1)
class Solution {
public:
    int NumberOf1Between1AndN_Solution(int n)
    {
        //每次去掉最高位递归
        if(n <= 0)
            return 0;
        int count = 0;
        int i = 1;
        // i表示该位上为1的所有可能情况
        for(i = 1;i <= n;i=i*10)
        {
            int a = n/i,b = n%i;
            count = count+(a+8)/10*i+(a%10==1)*(b+1);
        }
        return count;
    }
    
};

从数学规律入手,明显提高时间效率

剑指offer提供的第二种方法是观察数字的规律,利用递归的方法解题。
《剑指offer》题目汇总_第3张图片
如上图所示,可以看出从最低开始,其实是一个递归过程,如果求出来最低的位数,后面的可以直接根据前面的结果进行计算。【精髓,每次去掉最高位进行递归】与剑指offer思路类似,Java实现,我没有进行修改。

public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        if(n<0){
            return 0;
        }
        String str= Integer.toString(n);
        int result = getNumberOf1(str, 0);
        return result;
    }
    public static int getNumberOf1(String str,int index){
        int length = str.length()-index;
        if(length==1 && str.charAt(index)=='0'){
            return 0;
        }
        if(length==1){
            return 1;
        }
        //计算最高位的1
        int first = str.charAt(index)-'0';
        int result = 0;
        if(first>1){
            result += exp(length-1);
        }else if(first==1){
            result += 1 + Integer.parseInt(str.substring(index+1));
        }
        //计算除了最高位的其他位
        result += first *(length-1)*exp(length-2);
        //计算比如2345中0---345中1的个数进行递归
        result += getNumberOf1(str, index+1);
         
        return result;
    }
    public static int exp(int n){
        int result =1;
        while(n>=1){
            result*=10;
            n--;
        }
        return result;
    }
}

你可能感兴趣的:(数据结构与算法)