【剑指Offer】剑指offer题目----数组

【剑指Offer】剑指offer题目


本文为《剑指Offer》刷题笔记的总结篇,计划将牛客网上《剑指Offer》的66道题刷了一遍,以博客的形式整理了一遍,这66道题属于相对基础的算法题目,对于刷题练手是很好的实践,接下来会继续回到LeetCode,争取每天拿出一个小时,刷一到两道题。  

如果有什么问题,请多多指教。


前言

  主要对这66道题做一个总结,整体来看,这66道题,涉及到了常用的数据结构:数组、字符串、链表、树、二叉树、栈、队列,还有在编程中经常用到的数据操作和算法:循环、递归、查找、排序、回溯、动态规划、位运算。以下对这66道题做一个归类。   从这些题目中,我们可以学习到以下一些解题经验: 首先要真正理解题意,对于一些不确定的情况,比如输入输出格式,可以追问。 思考要全面,对于特殊情况,比如指针为null,输入为空等等情况,要充分考虑,保证代码的完整性和鲁棒性。 当遇到一个比较复杂的问题时,可以通过画图、举例或者分解来考虑,从具体的实例中总结普遍规律。 注重效率的考量,这66道题用蛮力法解决可能都不是很难,但是我们需要考虑的是时间效率和空间效率的平衡,以空间换时间有时候是一个不错的选择。 常用数据结构和数据操作是基础,要重点掌握,如树的遍历,排序,查找,递归等操作,在题目中反复用到,要深刻理解算法思想。

数组(共12道题目):

【剑指Offer】6、旋转数组的最小数字

在这里插入图片描述
解法一:
由于是个有序数组,可以考虑二分查找:

# -*- coding:utf-8 -*-
class Solution:
    def minNumberInRotateArray(self, rotateArray):
        # write code here
        if len(rotateArray)==0:
            return 0
        l=0
        r=len(rotateArray)-1
        while l<r:
            mid=(l+r)//2
            if rotateArray[mid]>rotateArray[r]:
                l=mid+1
            else:
                r=mid
        return rotateArray[l]

需要改进:
采用二分法解答这个问题,
mid = low + (high - low)/2
需要考虑三种情况:
(1)array[mid] > array[high]:
出现这种情况的array类似[3,4,5,6,0,1,2],此时最小数字一定在mid的右边。
low = mid + 1
(2)array[mid] == array[high]:
出现这种情况的array类似 [1,0,1,1,1] 或者[1,1,1,0,1],此时最小数字不好判断在mid左边
还是右边,这时只好一个一个试 ,
high = high - 1
(3)array[mid] < array[high]:
出现这种情况的array类似[2,2,3,4,5,6,6],此时最小数字一定就是array[mid]或者在mid的左
边。因为右边必然都是递增的。
high = mid
注意这里有个坑:如果待查询的范围最后只剩两个数,那么mid 一定会指向下标靠前的数字
比如 array = [4,6]
array[low] = 4 ;array[mid] = 4 ; array[high] = 6 ;
如果high = mid - 1,就会产生错误, 因此high = mid
但情形(1)中low = mid + 1就不会错误
改进代码:

# -*- coding:utf-8 -*-
class Solution:
    def minNumberInRotateArray(self, rotateArray):
        # write code here
        if len(rotateArray)==0:
            return 0
        l=0
        r=len(rotateArray)-1
        while l<r:
            mid=(l+r)//2
            if rotateArray[mid]>rotateArray[r]:
                l=mid+1
            elif rotateArray[mid]>rotateArray[r]:
                mid=mid-1
            else:
                r=mid
        return rotateArray[l]

javaj解法:
参考别人的代码思路:
解题思路:

本题的直观解法很简单,直接对数组进行一次遍历就可以找到最小值,复杂度为O(n),但是显然这不是本题的意图所在,因为没有利用到任何旋转数组的特性。

进一步分析,如果整个数组是有序的,那我们一定会想到用折半查找来实现。对于旋转数组,我们发现,它实际上可以划分为两个排序的子数组,而且前面数组的元素都不小于后面数组的元素,并且最小值正好就是这两个数组的分界线,由此,我们可以得出以下解决方法。

首先用两个指针low和high分别指向数组的第一个元素和最后一个元素,然后可以找到中间元素mid。对于这个中间元素,有以下两种情况:(1)该元素大于等于low指向的元素,此时最小的元素说明在mid的后面,可以把low=mid;(2)中间元素小于等于high指向的元素,那么最小元素在mid之前,可以high=mid。特别注意:这里不要+1或者-1,因为只有这样才能保证low始终在第一个数组,high始终在第二个数组。依次循环,当最后low和high相差1时,low指向第一个数组的最后一个,high指向第二个数组的第一个(即为我们要找的最小值)。

很明显,以上查找的时间复杂度为O(logN)。

在这里插入图片描述
除此之外,本题还有两个特殊情况:

将数组前0个元素移动到后面(相当于没有旋转,数组整体有序)。明显我们上面的分析没有包含这种情况,需要特殊处理,方法也很简单,将第一个元素和最后一个元素相比,若第一个元素小于最后一个元素,则说明最小值就是的第一个元素,可以直接返回。

首尾指针指向的数字和中间元素三者都相等时,无法判断中间元素位于哪个子数组,无法缩小问题规模。此时,只能退而求其次,进行顺序查找。
在这里插入图片描述

public int minNumberInRotateArray(int [] array) {
        /*
        三种情况:
        (1)把前面0个元素搬到末尾,也就是排序数组本身,第一个就是最小值
        (2)一般情况二分查找,当high-low=1时,high就是最小值
        (3)如果首尾元素和中间元素都相等时,只能顺序查找
        */
        int len=array.length;
        if(len==0)
            return 0;
        int low=0,high=len-1;
        if(array[low]<array[high]) //排序数组本身
            return array[low];
        while(low<high){
            int mid=low+(high-low)/2;
            if(array[low]==array[mid] && array[high]==array[mid])
                return minInOrder(array);
            if(array[mid]>=array[low])
                low=mid;
            else if(array[mid]<=array[high])
                high=mid;
            if(high-low==1)
                return array[high];
        }
        return -1;
    }
    public int minInOrder(int [] array) { //顺序查找
         int min=array[0];
         for(int num:array){
             if(num<min)
                 min=num;
         }
          return min;
     }

解法三:大佬的思路,orz~

public class Solution {
    public int minNumberInRotateArray(int[] array) {
        int i = 0, j = array.length - 1;
        while (i < j) {
            if (array[i] < array[j]) {
                return array[i];
            }
            int mid = (i + j) >> 1;
            if (array[mid] > array[i]) {
                i = mid + 1;
            } else if (array[mid] < array[j]) {
                j = mid; // 如果是mid-1,则可能会错过最小值,因为找的就是最小值
            } else i++;  // 巧妙避免了offer书上说的坑点(1 0 1 1 1)
        }
        return array[i];
    }
}

【剑指Offer】1、二维数组中的查找

在这里插入图片描述
针对这一题,想到遍历求解:

class Solution:
    # array 二维列表
    def Find(self, target, array):
        # write code here
        for i in range(len(array)):
            line = array[i]
            for j in range(len(line)):
                num = line[j]
                if num == target:
                    return (i, j)
        return False


if __name__ == "__main__":
    s = Solution()
    res = s.Find(4, [[2, 3, 4], [1, 2, 4], [4, 5, 6]])
    print(res)

当然这种就不是最优解。

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

解题思路:
  很明显,由于该二维数组上到下递增,左到右递增的特殊性,遍历整个矩阵进行查找不是该题目的意图所在。总结规律我们可以发现:应该从矩阵的右上角或者左下角开始查找。
  以右上角为例,首先选取右上角的数字,如果该数字等于要查找的数字,则查找过程结束;如果该数字大于要查找的数字,则说明该列其他元素都大于要查找的数字,便可以删掉该列;如果该数字小于要查找的数字,则说明该行其他元素也都小于要查找的数字,便可以删掉该行。
  这样,每一次比较都可以剔除一行或者一列,进而缩小查找范围,时间复杂度为O(n)。

比如在下面的二维数组中查找数字7,查找过程如下:
在这里插入图片描述
java 代码实现:

public class Solution {
    public boolean Find(int target, int [][] array) {
        /*
        思路:从左下角(或者右上角)开始查找,因为该行右边大于它,上边小于它,每次比较可以删除某一行或者某一列
        注意:左上和右下不可以,因为无法减小问题规模(行和列都无法删除)
        */
        if(array==null)
            return false;
        int row=array.length; //行数
        int col=array[0].length; //列数
        for(int i=row-1,j=0;i>=0&&j<col;){ //从左下角开始查找
            if(array[i][j]==target) //找到
                return true;
            else if(array[i][j]>target) //不可能在该行,跳过该行
                i--;
            else //不可能在该列,跳过该列
                j++;
        }
        return false;
    }
}

当然自己根据这个思路写了下python的代码:(左下角开始查找)

class Solution:
    # array 二维列表
    def Find(self, target, array):
        # write code here
        if len(array)==0:
            return  False
        row=len(array)-1
        col=0
        #左下角开始查找
        while row>=0 and col<=len(array[0])-1:
            if array[row][col]==target:
                return (row,col)
            elif array[row][col]>target:
                row-=1
            else:
                col+=1
        return False


if __name__ == "__main__":
    s = Solution()
    res = s.Find(7, [[1, 2, 8,9], [2, 4, 9,12], [4, 7, 10,13],[6,8,11,15]])
    print(res)

当然自己根据这个思路写了下python的代码:(右上角开始查找)

class Solution:
    # array 二维列表
    def Find(self, target, array):
        # write code here
        if len(array)==0:
            return  False
        row=0
        col=len(array[0])-1
        #右上角开始查找
        while col>=0 and row<=len(array)-1:
            if array[row][col]==target:
                return (row,col)
            elif array[row][col]>target:
                col-=1
            else:
                row+=1
        return False


if __name__ == "__main__":
    s = Solution()
    res = s.Find(7, [[1, 2, 8,9], [2, 4, 9,12], [4, 7, 10,13],[6,8,11,15]])
    print(res)

总结:
算法的技巧在于思维,不停的看到新的思维方式,才能想到不一样的解法。当然,看题也很重要。

【剑指Offer】13、调整数组顺序使奇数位于偶数前面

在这里插入图片描述
解法一:
我的以一个解法是:

#
# 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
#
# 
# @param array int整型一维数组 
# @return int整型一维数组
#
class Solution:
    def reOrderArray(self , array ):
        # write code here
        l=[]
        r=[]
        for i in array:
            if i%2==0:
                r.append(i)
            else:
                l.append(i)
        l.extend(r)
        return l
            

解法二:
首先,如果不考虑奇数和奇数,偶数和偶数的相对位置,那么我们有一种双指针解法来求解,类似于快排,维护两个指针,第一个指针指向数组的第一个数字,第二个指针指向数组的最后一个数字。第一个指针向后移,第二个指针向前移,如果第一个指针指向偶数,第二个指针指向的是奇数,则交换着两个数字,接着继续移动直到两指针相遇。
  上面的方法看似不错,但是对本题不适用,因为本题有相对位置不变的要求,直接交换会导致相对位置改变。因此,我们采用下面的思路来解决本题。
  本题解法:对数组进行遍历,设置两个指针even和odd,even指向当前第一个偶数,odd从这个偶数之后开始查找,找到第一个奇数,此时为了相对位置不变,不能直接交换even和odd,而是将从even到odd-1的元素都依次向后移一个位置,将odd指向的那个奇数放到even的位置。然后再找下一个偶数,重复这一过程,最终就可以将奇数都放到偶数的前面,并且保证了相对位置的不变。

public void reOrderArray(int [] array) {
        int len=array.length;
        int even=0,odd=0; //当前序列的第一个奇数和第一个偶数
        while(odd<len && even<len){
            while(even<len && array[even]%2!=0) //找到第一个偶数even
                even++;
            odd=even+1;
            //找偶数之后的第一个奇数
            while(odd<len && array[odd]%2==0)
                odd++;
            if(odd>=len)  //注意判断,防止溢出
                break;
            //把奇数取出来,从even到odd-1的元素都向后移
            int temp=array[odd];
            for(int i=odd;i>even;i--)
                array[i]=array[i-1];
            array[even]=temp; //奇数放在原来even的位置
            even++;
        }
    }

【剑指Offer】19、顺时针打印矩阵

【剑指Offer】28、数组中出现次数超过一半的数字

在这里插入图片描述
本题有以下三种方法可解:
  方法一:首先对数组进行排序,在一个有序数组中,次数超过一半的必定是中位数,那么可以直接取出中位数,然后遍历数组,看中位数是否出现次数超过一半,这取决于排序的时间复杂度,最快为O(nlogn)。
  方法二:遍历数组,用 HashMap 保存每个数出现的次数,这样可以从map中直接判断是否有超过一半的数字,这种算法的时间复杂度为O(n),但是这个性能提升是用O(n)的空间复杂度换来的。
  方法三(最优解法):根据数组特点得到时间复杂度为O(n)的算法。根据数组特点,数组中有一个数字出现的次数超过数组长度的一半,也就是说它出现的次数比其他所有数字出现的次数之和还要多。因此,我们可以在遍历数组的时候设置两个值:一个是数组中的数result,另一个是出现次数times。当遍历到下一个数字的时候,如果与result相同,则次数加1,不同则次数减一,当次数变为0的时候说明该数字不可能为多数元素,将result设置为下一个数字,次数设为1。这样,当遍历结束后,最后一次设置的result的值可能就是符合要求的值(如果有数字出现次数超过一半,则必为该元素,否则不存在),因此,判断该元素出现次数是否超过一半即可验证应该返回该元素还是返回0。这种思路是对数组进行了两次遍历,复杂度为O(n)。

解法一:

//思路2:用hashmap保存每个数出现的次数
    public int MoreThanHalfNum_Solution(int [] array) {
        if(array==null)
            return 0;
        Map<Integer,Integer> res=new HashMap<>();
        int len = array.length;
        for(int i=0;i<array.length;i++){
            res.put(array[i],res.getOrDefault(array[i],0)+1);
            if(res.get(array[i])>len/2)
                return array[i];
        }
        return 0;
    }

    //思路3:根据数组特点得到时间复杂度为O(n)的算法
    public int MoreThanHalfNum_Solution(int [] array) {
    	if(array==null||array.length==0)
            return 0;
        int len = array.length;
        int result=array[0];
        int times=1;
        for(int i=1;i<len;i++){
            if(times==0){
                result=array[i];
                times=1;
                continue;
            }
            
            if(array[i]==result)
                times++;
            else
                times--;
        }
        //检查是否符合
        times=0;
        for(int i=0;i<len;i++){
            if(array[i]==result)
                times++;
            if(times>len/2)
                return result;
        }
        return 0;
    }

解法二:
哈希法:显然可以先遍历一遍数组,在map中存每个元素出现的次数,然后再遍历一次数组,找出众数。

class Solution:
    def MoreThanHalfNum_Solution(self, numbers):
        dict={}
        for var in numbers:
            if var not in  dict.keys():
                dict[var]=1
            else:
                dict[var] += 1

        for key,value in dict.items():
            if value>len(numbers)/2:
                return key
        return None

if __name__ == "__main__":
    s = Solution()
    res = s.MoreThanHalfNum_Solution([1,2,3,2,2,2,5,4,2])
    print(res)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值