剑指offer(41---50)

文章目录

          • 41、和为s的连续正数序列
          • 42、和为s的两个数字
          • 43、左旋转字符串
          • 44、反转单词顺序列
          • 45、扑克牌顺子
          • 46、孩子们的游戏
          • 47、求1+2+3+...+n
          • 48、不用加减乘除做加法
          • 49、把字符串转换为整数
          • 50、数组中重复的数字

41、和为s的连续正数序列

小明很喜欢数学,有一天他在做数学作业时,要求计算出9~16的和,他马上就写出了正确答案是100。但是他并不满足于此,他在想究竟有多少种连续的正数序列的和为100(至少包括两个数)。没多久,他就得到另一组连续正数和为100的序列:18,19,20,21,22。现在把问题交给你,你能不能也很快的找出所有和为S的连续正数序列?

在看到这个题目时,我们可以想到一个类似比较简单的一个题目,就是和为s的两个数(在递增序列中),一般这种题目我们第一时间可以想到的方法就是固定一个数字,在其他数字中遍历看是否有何这个数相加为s的数,但是这种方式的时间复杂度为O(n^2),因此我们要考虑效率比较高的方法,可以定义一个指针i指向数组第一个数,一个指针j指向数组最后一个元素,如果arr[i]+arr[j]的结果是s,直接返回,如果大于s,则j向前走一步,否则i向后走一步,实现代码如下:

class Solution {
public:
    vector FindNumbersWithSum(vector array,int sum) {
        vector result;
        if(array.size()<2)//说明只有一个或者0个数
            return result;
        int begin=0;
        int end=array.size()-1;
        while(begin < end)
        {
            int cursum = array[begin]+array[end];
            if(cursum == sum)
            {
                //第一组就是乘积最小的
                result.push_back(array[begin]);
                result.push_back(array[end]);
                return result;
            }
            else if(cursum < sum)
                begin++;
            else
                end--;
        }
        return result;
    }
};

而这里这个问题是求和为s的连续正数序列
也可以利用上述的思想,我们初始化small为1,big为2,此时介于small和big之间的序列就是{1,2},序列和curname为3,如果从small到big的序列和curname等于s,直接将small到big的序列插入result,如果从small到big的序列和curname大于s,我们可以从序列中去掉较小的值,也就是增大small,此时curname就要减小small,一直增大small直到curname与sum相等,否则如果从small到big的序列和小于s,我们可以从序列中增大big,让这个序列包含更多的值,此时curname也要加上向后扩充的值(每次big++就是向后扩充一个,因为是连续的序列)这个序列至少有两个数字,因此small一直增加到(1+s)/2为止,因此可以实现代码:

class Solution {
public:
    vector > FindContinuousSequence(int sum) {
        vector> result;//存每一个结果序列
        if(sum<3)//说明少于两个数,无法计算
            return result;
        int small=1;//先让small初始化为1
        int big=2;
        int curname=small+big;//每次比较的就是curname和sum
        int middle=(1+sum)/2;
        while(small tmp;//存这一个序列
                for(int i=small;i<=big;++i)
                {
                    tmp.push_back(i);
                }
                result.push_back(tmp);
            }
            //说明curname与sum不相等
            //如果curname大于sum,说明要增大small(去掉较小的),也就是只要curname一直小于sum,就要一直增大small,只要small没有到(1+sum)/2
            while(curname>sum && small tmp;//存这一个序列
                    for(int i=small;i<=big;++i)
                    {
                        tmp.push_back(i);
                    }
                    result.push_back(tmp);
                }
            }
            //说明curname小于sum,big向后扩充,curname也向后加
            big++;
            curname+=big;
        }
        return result;  
    }
};
42、和为s的两个数字

输入一个递增排序的数组和一个数字S,在数组中查找两个数,使得他们的和正好是S,如果有多对数字的和等于S,输出两个数的乘积最小的。

这个题就是我们上面41题说的比较简单的那种情况,使用双指针的方法,这里要注意的是找到的第一组就是乘积最小的(使用数学证明,这里就不多说了)
实现代码:

class Solution {
public:
    vector FindNumbersWithSum(vector array,int sum) {
        vector result;
        if(array.size()<2)//说明只有一个或者0个数
            return result;
        int begin=0;
        int end=array.size()-1;
        while(begin < end)
        {
            int cursum = array[begin]+array[end];
            if(cursum == sum)
            {
                //第一组就是乘积最小的
                result.push_back(array[begin]);
                result.push_back(array[end]);
                return result;
            }
            else if(cursum < sum)
                begin++;
            else
                end--;
        }
        return result;
    }
};
43、左旋转字符串

汇编语言中有一种移位指令叫做循环左移(ROL),现在有个简单的任务,就是用字符串模拟这个指令的运算结果。对于一个给定的字符序列S,请你把其循环左移K位后的序列输出。例如,字符序列S=”abcXYZdef”,要求输出循环左移3位后的结果,即“XYZdefabc”。

看到这个题,很多人会想到那个经典的调整单词顺序问题,也就是将类似于“I am a student”转为“student a am I”,这个问题一般采用的就是先将字符串整体逆转,变为“tneduts a ma I”,然后每个单词的顺序即可,此时实现它的代码为:

void Reverse(string& str, int begin, int end)
{
	if (str.size() == 0)
		return;
	while (begin < end)
	{
		char tmp = str[begin];
		str[begin] = str[end];
		str[end] = tmp;
		begin++;
		end--;
	}
}
string ReverseSentence(string& str)
{
	if (str.size() == 0)
		return nullptr;
	Reverse(str, 0, str.size()-1);//先整体逆转
	//翻转每个单词
	int begin = 0;//指向单词的起始位置
	int end = 0;//指向单词的终点位置
	while (str[begin] != '\0')
	{
		if (str[begin] == ' ')//先将该单词前面的空格处理掉
		{
			begin++;
			end++;
		}
		else if (str[end] == ' ' || str[end] == '\0')//如果end遇到空格或者\0,说明begin到end-1是一个单词
		{
			Reverse(str, begin, --end);
			begin = ++end;//begin走到end+1的位置
		}
		else//说明还不是一个单词,end继续往后走
			end++;
	}
	return str;
}

那么这里我们要处理的是左旋字符串的问题,可以从上面的问题得到启发,也就是比如要将abcXYZdef左移三个,成为XYZdefabc,也就是我们将abc和XYZdef作为两部分,然后整体逆转,然后分别逆转即可,也就是先逆转为fedZYXcba,然后分别逆转成为XYZdefabc,
实现代码:

class Solution {
public:
    string LeftRotateString(string str, int n) {
        if(str.size()
44、反转单词顺序列

43题已经介绍过了,这里就不多做介绍了,在牛客网上实现的代码为:

45、扑克牌顺子

LL今天心情特别好,因为他去买了一副扑克牌,发现里面居然有2个大王,2个小王(一副牌原本是54张_)…他随机从中抽出了5张牌,想测测自己的手气,看看能不能抽到顺子,如果抽到的话,他决定去买体育彩票,嘿嘿!!“红心A,黑桃3,小王,大王,方片5”,“Oh My God!”不是顺子…LL不高兴了,他想了想,决定大\小 王可以看成任何数字,并且A看作1,J为11,Q为12,K为13。上面的5张牌就可以变成“1,2,3,4,5”(大小王分别看作2和4),“So Lucky!”。LL决定去买体育彩票啦。 现在,要求你使用这幅牌模拟上面的过程,然后告诉我们LL的运气如何, 如果牌能组成顺子就输出true,否则就输出false。为了方便起见,你可以认为大小王是0。

把这5个牌当成一个数组中的元素,要连成一个顺子(大小王暂且当做0,用来填补需要的牌),首先我们可以将数组排好序,此时如果有0(大小王),就一定在数组的最前面,统计0的个数,剩下的元素如果有对子,就直接返回false,因为肯定不能构成顺子了,然后countGap统计相邻两个元素之间差几个数,如果0的个数大于或者等于countGap,则表示可以填补完空缺的元素,返回true,否则返回false。
实现代码:

class Solution {
public:
    bool IsContinuous( vector numbers ) {
        if(numbers.size()<5)
            return false;
        //先给数组排序,按顺序
        sort(numbers.begin(),numbers.end());
        int countZero=0;//计算0的个数
        int countGap=0;//计算排序后的数组相邻两个数之间间隔几个数
        for(int i=0;i=countGap)
            return true;
        return false;
    }
};
46、孩子们的游戏

这个就是经典的约瑟夫环问题,即将0,1,2…n-1这n个数围成一个圈,从0开始删除第m个数(下一次从删除的那个数的下一个开始计算要删除的数),求剩下的最后一个数,例如0,1,2,3,4这5个数,从0开始删除第3个数,则第一次删除2,剩0,1,3,4,再从3开始删除第三个数,即第二次删除0,剩1,3,4,再从1开始删除第三个数,即第三次删除4,剩1,3,再从1开始删除第三个数,即第四次删除1,最后剩下的数是3。

由于这里要将元素围成一个圈,因此我们第一时间想到的是使用一个数据结构来模拟这个圆圈,想到使用环形链表(如果可以使用模板库中的数据结构,就使用std::list来实现环形链表,但是要注意list本身不是一个环形链表,因此当迭代器走到链表末尾时,要将迭代器移到链表的头部),使用it迭代器用来表示当前要删除的元素的位置,next迭代器表示删除的元素的下一个元素的位置,每次it走到m-1处(要是走到末尾了就直接赋为begin()),然后保存它的下一个元素的迭代器next,然后删除it所在的元素,it走到next,循环直到环形链表只剩下1个元素即可。

class Solution {
public:
    int LastRemaining_Solution(int n, int m)
    {
        if(n<1 || m<1)
            return -1;
        list L;
        for(int i=0;i::iterator it=L.begin();//迭代器,指向要删除的元素
        while(L.size()>1)
        {
            for(int i=1;i::iterator next=++it;//由于++it会产生副作用,因此下一次要先--it,然后删除,然后it走到next处
            if(next == L.end())
                next=L.begin();
            --it;
            L.erase(it);
            it=next;
        }
        return *(it);
    }
};
47、求1+2+3+…+n

求1+2+3+…+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。

这个题不难实现,但是这里有众多要求,因此我们不能使用循环,而要使用递归还要考虑终止条件,因此也不能使用,而我们可以想象实现n次累加操作就可以实现,因此可以使用构造函数,构造n个对象,调用了n次构造函数,在构造函数中实现累加相关的代码,记得要将成员变量设置为static的,这样才能保存上一次的结果,实现代码:

class H{
public:
    H()
    {
        ++n;
        sum += n;
    }
    static int GetSum()
    {
        return sum;
    }
    static void Reset()
    {
        n=0;
        sum=0;
    }
private:
    static int n;
    static int sum;//使用static,表示共用这一个对象,可以保存上一次的结果
};
int H::n=0;
int H::sum=0;//初始化
class Solution {
public:
    int Sum_Solution(int n) {
        H::Reset();//由于有多个测试用例,所以在下一次输入用例时都要重新将n和sum置为0
        H* a=new H[n];
        delete[]a;
        a=nullptr;
        return H::GetSum();
    }
};
48、不用加减乘除做加法

写一个函数,求两个整数之和,要求在函数体内不得使用+、-、*、/四则运算符号。

这个题不能用四则运算,因此就考虑到使用位运算,如果两个数的二进制位相加,第一步我们先不记进位,直接相加,那么例如5+17也就是00101+10001,此时不记进位,就为10100为12,其实就相当于异或操作,第二步就是记上进位,想要记上进位,只有1+1时才会有进位,因此只有哪一位二进制位相与为1时才能表示这一位要进位,因此这里要记上进位就将两个数相与,进位是想高位进位,因此相与后的结果向左移动一位,即(5&7)<<1=00010,第三步就是将前两步继续重复,直到没有进位,实现代码:

class Solution {
public:
    int Add(int num1, int num2)
    {
        int sum;
        int carry;
        do{
            sum=num1^num2;
            carry=(num1&num2)<<1;
            num1=sum;//等于一步步将结果加给num1
            num2=carry;
            
        }while(num2!=0);
        return num1;
    }
};
49、把字符串转换为整数

将一个字符串转换成一个整数,要求不能使用字符串转换整数的库函数。 数值为0或者字符串不是一个合法的数值则返回0

这个问题就是模拟实现一个atoi函数,我们知道字符转为整数使用num=num*10+(*str-‘0’)即可,但是我们还要考虑一些条件,例如:
(1)如果当前字符<'0’或者>‘9’,就是不合法的,num直接为0,循环结束;
(2)考虑正负数,这个在遍历第一个字符时就要确定;
(3)考虑溢出条件,如果是正数,不能大于0x7FFFFFFF,如果是负数,绝对值不能大于0x80000000;
(4)我们要对非法输入进行处理,atoi函数中处理是使用一个全局变量来区别,因此这里我们实现的时候也要定义一个全局变量g_Status来区分当前的输入是否是合法输入。

class Solution {
    //这个问题要考虑的方面挺多的,首先要知道atoi这个函数处理非法输入是怎么处理的,也就是str为空或者为"0"时是如何处理的
    //其实atoi在处理时使用一个全局变量,如果输入为"0",不用设置,如果为非法输入,要进行设置
    //接下来还要考虑字符串中是否含有"+"/"-"这样的字符,如果有,要考虑正负数
    //还有如果字符串中某个字符<="0"或者>="9",直接为0,结束循环
    //还要考虑溢出问题,也就是如果是正数大于0x7FFFFFFF或者为负数大于0x80000000,也直接为0,循环结束
public:
    enum Status{k_Valid=0,k_Invalid};//用来表示是否是非法输入
    int g_Status=k_Valid;//用这个全局变量来表示是否是非法输入
    int StrToInt(string str) {
        g_Status=k_Invalid;//表明此时还是非法输入
        long long result=0;//结果
        const char* Cstr=str.c_str();
        if(Cstr!=nullptr && *Cstr!='\0')
        {
            //考虑正负数的情况
            int flag=1;//表示为正数
            if(*Cstr=='+')
                Cstr++;
            else if(*Cstr=='-')
            {
                Cstr++;
                flag = -1;//表示为负数
            }
            //开始转换
            while(*Cstr!='\0')
            {
                //继续考虑是否有<='0'和>='9'的情况
                if(*Cstr>='0' && *Cstr<='9')//说明合法
                {
                    g_Status=k_Valid;
                    result=result*10+(*Cstr-'0');
                    Cstr++;
                    //考虑溢出问题
                    if(((flag>0) && (result>0x7FFFFFFF)) || ((flag<0) && (result>0x80000000)))
                    {
                        g_Status=k_Invalid;//不合法
                        result=0;
                        break;
                    }
                }
                else//说明不合法
                {
                    g_Status=k_Invalid;
                    result=0;
                    break;
                }
            }
            if(g_Status==k_Valid)
            {
                result=flag*result;//将正负数区别好
            }
        }
        return (int)result;
    }
};
50、数组中重复的数字

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

拿到这个题,我们第一时间可以想到利用哈希的方式来实现,也就是这个unordered_map中与数组中的元素建立对应关系,并且其对应的属性要么是true,要么是false,遍历元素,如果出现过当前数字,直接返回true,如果没有出现过,加入unordered_map

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) {
        unordered_map m;
        for (int i = 0; i 

但是这种方式使用了额外的空间,我们如果不想使用额外的空间,就可以例如:
数字都是在0到n-1,假设没有重复数字,那么将这个数组排完序,每个数都在唯一的位置,并且numbers[i]=i;
扫描整个数组,假设numbers[i]=i,即这个数字在他该在的位置,继续扫描下一个;
如果numbers[i]!=i,我们应该将numbers[i]放在他该放在的位置,即numbers[numbers[i]];
此时如果numbers[i]==numbers[numbers[i]],说明重复了,就找到了重复的数字,如果numbers[i]!=numbers[numbers[i]],就将numbers[i]放在numbers[numbers[i]]上去,处理numbers[numbers[i]],直到重复或者和i相等。

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;ilength-1)
                return false;
        }
        for(int i=0;i

你可能感兴趣的:(#,算法)