剑指offer刷题详细分析:part6:26题——30题

  • 剑指offer所有题目详解,可访问我的github项目:KongJetLin-offer

  • 目录

  1. Number26:二叉搜索树与双向链表
  2. Number27:字符串的排列
  3. Number28:数组中出现次数超过一半的数字
  4. Number29:最小的k个数
  5. Number30:连续子数组的最大和

题目26 二叉搜索树与双向链表

  题目描述:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表。要求不能创建任何新的结点,只能调整树中结点指针的指向。

  分析:
  1)要求双向链表排序,我们利用二叉搜索树的特性,中序遍历二叉搜索树,就可以得到排序的结点。
  2)我们从二叉搜索树取出一个结点构建链表,设置该结点的左指针指向前一个结点,右指针指向下一个结点,其实我们在将取出的结点连接到双向链表的时候,只需要考虑前一个结点的右指针指向当前新的结点、当前新结点的指针指向前一个结点即可。
  至于当前新结点的右指针不需要管,如果有下一个结点插入双向链表,那么当前新结点的指针再设置指向下一个结点;如果没有下一个插入,当前结点的右指针则什么都不指向(null),此时已经是原来二叉搜索树中序遍历的最后一个结点。
  其实对于第一个结点和最后一个结点,他们在二叉搜索树中左右指针本来都指向null,因此他们有一边不指向任何结点并不会影响链表结构!
  有疑问就在大脑里面想象一下整个过程即可!
  代码如下:

private TreeNode head;//创建一个头结点,用于返回双向链表头结点
    //由于插入一个链表,我们只需要考虑当前结点左指针 和 前一个结点右指针,那么我们只需要创建一个代表前一个结点的pre即可
    private TreeNode pre;

    public TreeNode Convert(TreeNode pRootOfTree)
    {
        //使用中序遍历方式找出二叉搜索树所有结点,并一个个连接到双向链表上
        inOrder(pRootOfTree);
        return head;//返回双向链表头结点
    }
    
    private void inOrder(TreeNode node)
    {
        if(node == null)
            return;//如果二叉搜索树结点遍历完,不需要再向双向链表添加结点,直接结束递归
        
        //中序遍历左子树
        inOrder(node.left);

        /*
        对于遍历到的当前结点,我们先判断前一个结点pre是否存在,
        如果不存在,说明在双向链表中,当前结点是第一个结点,将当前结点赋予pre;
        如果存在,说明在双向链表中,当前结点前面有结点,将pre.right指向当前结点node,将node.left指向pre,这样当前结点就连接到双向链表,
        此时,对于下一个插入的结点当前结点就是前一个结点,因此将pre指向node,便于插入下一个结点。
        
        另外,如果pre=null,说明node是双向链表第一个结点,将其赋予head
         */
        if(pre != null)
        {
            pre.right = node;
            node.left = pre;
            pre = node;
        }
        else
        {
            pre = node;
            head = node;
        }

        //中序遍历右子树
        inOrder(node.right);
    }

题目27 字符串的排列

  题目描述:输入一个字符串,按字典序打印出该字符串中字符的所有排列。例如输入字符串abc,则打印出由字符a,b,c所能排列出来的所有字符串abc,acb,bac,bca,cab和cba。
  输入描述:输入一个字符串,长度不超过9(可能有字符重复),字符只包括大小写字母。

  分析:我们可以把整个流程分为2步:
1)求所有可能出现在第一个位置的字符,即把第一个字符与后面的字符依次交换。(for循环)
2)将某一个字符固定,求后面所有字符的排列。
  而求后面所有字符的排列,我们仍然可以将其分解为上面2步。这是一个递归的过程,直到字符串的结尾,我们停止递归。
  如下图:
在这里插入图片描述
  注意点:
1)在每个分支进行完后,要将交换过的元素复位,从而不会影响其他分支;
2)题目指出可能有字符重复,所以分支末尾可能有重复的序列,在加入ArrayList要进行去重判断,不重复再加入。

  代码如下:

public class OfferGetTest27
{
    //将返回结果的ArrayList定义在外面,避免下面的函数传递太多的参数
    ArrayList<String> result = new ArrayList<>();

    public ArrayList<String> Permutation(String str)
    {
        if(str==null || str.length()==0)
            return result;
        //注意,这里返回result这个ArrayList,不要返回null。因为当 str=""的时候,返回ArrayList,里面也是"",而不是null!
        Permutation(str.toCharArray() , 0);//index从0开始
        Collections.sort(result);//最后,将ArrayList的字符串排列一下,因为测试用例的字符串是有顺序的,不排列最后无法通过!

        return result;
    }

    //从strArr字符串数组的index位置开始置换字符
    private void Permutation(char[] strArr , int index)
    {
        /**
        当 index=strArr.length,说明递归到字符数组的结尾,需要将这个排列的字符数组转换为字符串,
         当然,由于题目给定的字符串里面可能有重复的字符,我们这里获取字符有可能与前面获取到的字符重复,
         因此在放入 result 之前需要先判断 result 里面是否存在这个字符串。
         */
        if(index == strArr.length-1)//遍历到最后一个字符,不需要再替换后面的字符(也不需要替换自己)
        {
            String str = String.valueOf(strArr);//将这个顺序的字符数组变为字符串
            if(!result.contains(str))
                result.add(str);//字符串不存在才将其加入result
        }
        else
        {
            /**
            如果没有递归到字符数组的末尾,我们将index位置的字符逐个与后面的字符替换,
             然后将当前index位置的字符固定,通过 Permutation() 方法,递归寻找index+1位置开始的所有字符的可能排列,
             直到递归到字符数组的末尾。
             注意:
             1)这里需要从index位置开始替换,即index位置的字符保持不变,因为这也是一种情况;
             2)当index+1位置递归返回后,我们需要将原来的替换复原,如果不一层一层递归在替换后都复原,
                递归后的字符数组就是乱序的,从而影响其他分支的替换。(这个过程参考解析的图)
             */
            for (int i = index; i <= strArr.length-1 ; i++)
            {
                //index位置的字符在循环中逐个替换 i=index,index+1,...,strArr.length-1位置的字符
                swap(strArr , index , i);
                //替换一个字符后,将当前index位置的字符固定,从index+1位置开始递归替换
                Permutation(strArr , index+1);
                /**
                为了不影响其他分支,我们在递归搜索完 index+1 开始位置所有可能的字符排列情况后,将替换还原,
                 而递归中也会将替换还原,那么到这里,就变成没有替换 index与i 位置字符之前的情况,
                 那么我们下面就可以将 index与i+1 进行替换,这样就可以找到index位置所有可能的字符(想象一下这个过程!)
                 */
                swap(strArr , index , i);
                //另外,这里不需要考虑重复,因为递归到最后,重复的字符串会被剔除
            }
        }
    }
    //替换方法
    private void swap(char[] arr , int p1 , int p2)
    {
        char temp = arr[p1];
        arr[p1] = arr[p2];
        arr[p2] = temp;
    }

    public static void main(String[] args)
    {
        ArrayList<String> ab = new OfferGetTest27().Permutation("ab");
        for (String s : ab)
        {
            System.out.println(s);
        }
    }
}

题目28 数组中出现次数超过一半的数字

  题目描述:数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。例如输入一个长度为9的数组{1,2,3,2,2,2,5,4,2}。由于数字2在数组中出现了5次,超过数组长度的一半,因此输出2。如果不存在则输出0。

  多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。
  使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素相等时,令 cnt++,否则令 cnt–。如果前面查找了i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于i / 2 的话 cnt 就一定不会为 0 。
此时我们用于统计的元素,有可能是majority,也有可能不是majority,但是剩下的元素中,肯定有majority,且剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找剩下的元素,就能找出 majority。
  当然还可以使用复杂度为O(n^2)的暴力法,不赘述!

public int MoreThanHalfNum_Solution(int [] array)
    {
        //我们使用数组第一个元素作为比较元素
        int comNum = array[0];

        /**
         * 使用count来计算当前比较元素的个数,初始值为1个,设数组长度是n
         * 1)当count=0的时候,说明前i+1个数组元素中(数组元素从0开始),比较元素的个数等于其他元素的个数。此时比较元素可能是majority,也有可能不是majority。
         * 如果比较元素是 majority,由于 majority 大于n/2,那么后面的 n-i-1 个元素中,majority的数量肯定大于一半;
         * 如果比较元素不是 majority,其他元素(包括majority)个数小于此时的比较元素,那么后面的 n-i-1 个元素中,majority的数量肯定大于一半;
         * 因此我们可以将比较元素进行替换:comNum = array[i]; ,继续从 i+1 开始,进行下面的遍历。
         * 遍历到数组最后,一定会有count一直大于0的情况,此时的比较元素就是majority。
         *
         * 2)替换的时候,为什么不把比较元素替换为:array[i+1],不是说从第 i+1 遍历比较吗?
         * 其实这里可以替换为 comNum = array[i+1],但是我们下一次循环又会遇到 array[i+1],再次比较就出错。(其实可以设置)
         * 因此我们此时将比较元素设置为:array[i],
         * 其实我这里考虑的是,会不会因为从 i 开始而不是从 i+1 开始,导致从i到n-1的元素中majority个数小于一半。
         *  其实不会,因为如果array[i]是majority,那么从i到n-1的元素中,majority的个数肯定大于一半;
         * 如果array[i] 不是majority,很快它遇到与他不同的元素(遇到的有可能是majority,也有可能不是majority),
         * 遇到的元素会使得比较元素再次替换,那么剩下的元素中majority的数量肯定大于一半。
         * 例如:
         * i+1 与 i 不同,那么此时比较元素替换为 array[i+1],那么从i+1到n-1的元素中,majority的个数肯定大于一半;
         * 如果 i+1 与 i 相同,很快就会大量不同的其他元素,那么剩下的元素中majority的个数还是大于一半!!!
         */
        for (int i = 1,count = 1; i < array.length ; i++)
        {
            if(array[i] == comNum)
                count++;
            else
                count--;

            if(count==0)
            {
                //将array[i]设置为比较元素,注意设置count=1,即此时比较元素个数为1
                comNum = array[i];
                count = 1;
            }
        }
        /*
        结束循环的时候,我们会获取到最后的comNum,此时这个comNum的count>=1,但是我们无法提供count的值进行判断;
        此时如果原来的数组中存在majority,那么comNum在数组中的数量肯定大于 n/2,且comNum就是majority;
        如果如果原来的数组中不存在majority,那么comNum在数组中的数量肯定不于 n/2;
        我们计算 comNum在数组中的数目,进行比较即可。
         */

        int num = 0;
        for (int i : array) {
            if(i == comNum)
                num++;
        }
        return num>array.length/2 ? comNum : 0;
    }

题目28 数组中出现次数超过一半的数字

  题目描述:输入n个整数,找出其中最小的K个数。例如输入4,5,1,6,2,7,3,8这8个数字,则最小的4个数字是1,2,3,4,。

  使用快排、计数排序、最大堆优先队列实现,各个代码如下:
  最大堆,时间复杂度是:O(nlogn)

public static ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k)
    {
        ArrayList<Integer> res = new ArrayList<>();

        //注意对特殊情况进行判断。
        if(input == null || input.length == 0 || k > input.length || k <= 0)
            return res;

        //记住java实现最大堆用最小堆加上Lambda表达式:(o1, o2) -> o2-o1  即可
        Queue<Integer> queue = new PriorityQueue<>((o1, o2) -> o2-o1 );

        //将数组中的元素放入堆中(注意只放k个元素)
        for (int i = 0; i < input.length; i++)
        {
            /**
            特别注意,这里 queue.size() < k,而不是queue.size() <= k。
             在添加第k个之前,queue.size()=k-1<k,进入循环,添加第k个,随后 queue.size() =k跳出循环,刚刚好添加k个。
             如果 queue.size() <= k,在添加k个后还满足循环,会添加第k+1个!
             */
            if(queue.size() < k)
            {
                queue.add(input[i]);
            }
            else
            {
                if(input[i] < queue.peek())
                {
                    //将队首(堆顶)元素移除,将更小的元素加入堆
                    queue.remove();
                    queue.add(input[i]);
                }
            }
        }

  快排,时间复杂度:O(nlogn)

public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k)
    {
        ArrayList<Integer> res = new ArrayList<>();
        if(input == null || input.length==0 || k>input.length || k<=0)
            return res;
        quickSort(input , 0 ,input.length-1);

        for (int i = 0; i < k ; i++)
        {
            res.add(input[i]);
        }

        return res;
    }

    private void quickSort(int[] arr , int left , int right)
    {
        //当 left>= right 的时候,结束递归
        if(left >= right)
            return;
        if(left < right)
        {
            //找去区间 left到right的中轴元素下标mid,并将小于中轴元素的元素放在mid左边,大于他的放在mid右边
            int mid = partition(arr , left , right);
            quickSort(arr , left , mid-1);
            quickSort(arr , mid+1 , right);
        }
    }

    private int partition(int[] arr , int left , int right)
    {
        int pivot = arr[left];//随机取最左边的元素为中轴元素
        int start = left+1;
        int end = right;

        while (true)
        {
            //找到左区间第一个大于 pivot 的元素
            while (start<=end && arr[start]<=pivot)
                start++;
            //找到右区间第一个小于于 pivot 的元素
            while (start<=end && arr[end]>pivot)
                end--;

            //若start>end ,说明中轴元素左右两边元素摆放完毕,结束循环
            if(start>end)
                break;
            //如果没有跳出循环,则交换start与end的元素
            swap(arr , start ,end);
        }

        //跳出循环后,此时end为中轴元素应该放的位置,将left与end互换元素,并放回中轴元素下标:end
        swap(arr , left ,end);
        return end;
    }

    private void swap(int[] arr , int i , int j)
    {
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

  计数排序,时间复杂度:O(n+k),这会多花费:O(k)的空间,其中k是新数组的大小。

ArrayList<Integer> res = new ArrayList<>();
        if (input == null || input.length == 0 || k > input.length || k <= 0) return res;

        //1、先找出input最大值最小值
        int min = input[0];
        int max = input[0];

        for (int i = 1; i < input.length ; i++)
        {
            if(input[i] < min)
                min = input[i];
            if(input[i] > max)
                max = input[i];
        }

        //2、随后,求出临时数组的大小,并构建临时数组
        int length = max-min+1;
        int[] temp = new int[length];

        //3、遍历input数组,将input数组元素作为temp数组下标,个数为特莫数组下标元素对应的值
        for (int i = 0; i < input.length ; i++)
        {
            //注意,藤牌数组区间从 0到max-min,而input数组元素从 min到max,因此应该减去min
            temp[input[i]-min]++;
        }

        int index = 0;
        //将temp元素取出放入input
        for (int i = 0; i < temp.length ; i++)
        {
            while (temp[i]>0)
            {
                input[index++] = i + min;//注意将min加回去
                temp[i] --;
            }
        }

        for (int i = 0; i < k ; i++)
        {
            res.add(input[i]);
        }

        return res;

题目30 连续子数组的最大和

  题目描述:HZ偶尔会拿些专业问题来忽悠那些非计算机专业的同学。今天测试组开完会后,他又发话了:在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和,你会不会被他忽悠住?(子向量的长度至少是1)

方法1:暴力法
  如下,暴力遍历所有序列并对比,时间复杂度是O(n^2)

public static int FindGreatestSumOfSubArray1(int[] array) {
        //maxSum不能赋值为0,有可能整个数组都是负数
        //maxSum赋值为最开始的子序列array[0],如果没有找到比他更大的子序列,array[0]就是最大的子序列,否则就赋值为其他更大的子序列
        int maxSum = array[0];

        //外循环用于调整从哪个下标开始,内循环用于记录以这个下标为开始的所有子序列的和
        for (int i = 0; i < array.length ; i++)
        {
            //thisSUm 循环记录所有以j下标为开头的子序列的值,当找到某一个子序列 thisSum>maxSum 的时候,我们就将这个子序列的值赋予maxSum
            //由于下一个内循环开始之前,开始下标变换,因此thisSum必须重新赋值为0
            int thisSum = 0;
            for (int j = i; j < array.length ; j++)
            {
                thisSum += array[j];
                if(thisSum > maxSum)
                    maxSum = thisSum;
            }
        }
        return maxSum;
        //运行时间:12ms,占用内存:9412k
    }

方法2:动态规划(推荐)
  先上动态规划的公式:

max(dp[i]) = getMax( max(dp[i-1]) + arr[i] , arr[i] )

  首先,dp[i] 就是以数组下标为 i 的数做为结尾的最大子序列和,注意是以 i 为结尾

比如说现在有一个数组 arr = {6,-3,-2,7,-15,1,2,2},我们使用一个 maxSum 来记录当前最大的子序列。
	dp[0] 就是以下标0为结尾的子序列的最大和,显然只有一个,我们记为 dp[0] = arr[0] = 6,此时maxSum = dp[0]。
	dp[1] 就是以下标1为结尾的子序列的最大和,那么 dp[1] = Max{dp[0]+arr[1] , arr[1]},既如果 dp[0]<0 ,dp[1]=arr[1],否则dp[1]=dp[0]+arr[1]。
	我们求出以下标1位结尾的子序列的最大和后,判断dp[1]与maxSum的大小,如果dp[1]>maxSum,我们将maxSum=dp[1]....
	dp[n] 就是以下标n为结尾的子序列的最大和,那么 dp[n] = Max{dp[n-1]+arr[n] , arr[n]},既如果 dp[n-1]<0 ,dp[n]=arr[n],否则dp[n]=dp[n-1]+arr[n]。
	我们求出以下标n位结尾的子序列的最大和后,判断dp[n]与maxSum的大小,如果dp[n]>maxSum,我们将maxSum=dp[n].....
	结论:
	1)我们只需要遍历数组,找到数组所有下标对应的以该下标为结尾的子序列的最大和,这就相当于判断完了数组中所有的子序列。并且,我们使用maxSum保存了这些子序列的最大值。
	2)对于 dp[n]:以下标n为结尾的子序列的最大和,我们必须先找到dp[n-1]:以下标n-1为结尾的子序列的最大和,dp[n-1]已经将前面的所有情况覆盖并找到最大和,此时dp[n]的最大值只有两种情况:dp[n-1]+arr[n] 或者 arr[n]

  动态规划法的时间复杂度是O(n)。
  实现代码如下:

public static int FindGreatestSumOfSubArray2(int[] array) {
        //注意,maxSum目前也应该记录为array[0],不能是0,因为可能整个数组值都小于0!
        int maxSum = array[0];//用于记录最大子序列和.
        int thisSum = array[0];//用于记录 以数组当前下标为结尾的最大子序列和,我们将其初始化赋值为dp[0]

        //从dp[1]开始查找
        for (int i = 1; i <array.length ; i++)
        {
            if(thisSum < 0)//如果dp[n-1]<0
                thisSum = array[i];//更新dp,此时dp[n]=array[n]
            else//如果dp[n-1]>0
                thisSum += array[i];//更新dp,此时dp[n]=array[n]+dp[n-1]

            //更新maxSUm值,最后才能得到数组中的最大子序列
            if(maxSum<thisSum)
                maxSum = thisSum;//当当前下标的dp[n]比 以之前下标结尾的最大子序列要大,那么就将maxSum更新为dp[n]
        }

        return maxSum;

        //运行时间:14ms,占用内存:9396k
    }

  还有一种递归分治的方法,参考文章:添加链接描述
  当然这篇文章的方法是对的,但是里面有的步骤写错,可以参考我在文章评论指出的错误!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值