算法题摘录四

转载请注明原文地址:http://www.cnblogs.com/ygj0930/p/6434201.html

1:把一个数组中的数字排成一个数,输出能排成的最小/大数字。

这道题,需要自定义数组的排序规则——不是按数值大小排序,而是按相邻两数拼接结果的大小来排,然后对数组进行重新排列。最后得到的数组按顺序拼接就是最小结果。这是利用了传递性原理。如果a<b<c,则ab<ba,那么abc<cab<cba。

//自定义排序规则:字符串o1与o2的排序规则是二者拼接结果小的在前
    public class MyComparator implements Comparator<String> {
        public int compare(String o1, String o2) {
            String contact1=o1+o2;
            String contact2=o2+o1;
            return contact1.compareTo(contact2);
        }
    }
    public void printMinNumber(int[] nums){
        String[] numStrings=new String[nums.length];
        //把整数数组转化为字符串数组来比较,避免数值过大时,拼接结果大于int的范围
        //这是一个技巧:以后凡是遇到数字超出范围的都转化为字符串来模拟数字!
        for(int i=0;i<nums.length;++i){
            numStrings[i]=String.valueOf(nums[i]);
        }
        //按自定义的规则把数字数组排序
        Arrays.sort(numStrings, new MyComparator());
        //排序后的数组拼接出来的数字就是最小数字
        StringBuilder builder=new StringBuilder();
        for (String string : numStrings) {
            builder.append(string);
        }
        System.out.println(builder.toString());
    }
    

2:求第N个丑数

丑数是指:只能表示成2或3或5相乘的形式的数。比如4=2*2,6=2*3.而14=2*7则不是。1是第一个丑数。

传统解法:先定义一个判断是不是丑数的函数isUgly(num):把num不断整除2或3或5直到1则是丑数,如果不能整除2、3、5或者整除到最后num不是1,则说明不是丑数。然后从1开始遍历,判断当前数是不是丑数......直到找到第N个丑数进行输出。

优化解法:从空间换时间,保存已经找到过的丑数。因为丑数=另一个丑数*2、3、5得到的,这样就方便求丑数而不必从1开始遍历找丑数。这样问题的关键就成了找到新丑数插入新数组并保持新数组维持增序排列,直到找到第N个丑数。

假设目前数组中已有按顺序排列的丑数,最大的为M。我们来确定下一个丑数:

把数组中从第一个丑数开始逐个X2,得到一系列丑数,其中<M的已经在数组中了,我们只需取第一个大于M的,记为M2;

同理,从第一个元素开始逐个X3,取第一个大于M的为M3;乘以5取第一个大于M的为M5。

那么下一个丑数就是在M2,M3,M5之间的最小者,把它插入数组,更新当前最大者;

重复上面步骤,从上一次找到M2,M3,M5的地方分别再往前移动下标并乘以2,3,5找到大于新的M的M2,M3,M5去确定下一个丑数......直到找到第N个丑数;

public int findUglyNumber(int K){
        int[] uglyNumbers=new int[K];
        uglyNumbers[0]=1;
        int index_2=0;
        int index_3=0;
        int index_5=0;
        int next_index=1;
        while(next_index<K){
            //确定下一个最大丑数
            int min=Math.min(Math.min(uglyNumbers[index_2]*2, uglyNumbers[index_3]*3), uglyNumbers[index_5]*5);
            uglyNumbers[next_index]=min;
            //寻找使得乘以2,3,5下一个大于最大丑数的下标
            while(uglyNumbers[index_2]*2<=uglyNumbers[next_index]){
                index_2++;
            }
            while(uglyNumbers[index_3]*3<=uglyNumbers[next_index]){
                index_3++;
            }
            while(uglyNumbers[index_5]*5<=uglyNumbers[next_index]){
                index_5++;
            }
            //更新下一个要确定的丑数的下标
            ++next_index;            
        }
        return uglyNumbers[next_index-1];
    }

3:找到第一个出现一次的字符

有很明显的映射关系:字符——出现次数。直接用哈希map来解决即可。

第一次遍历所有字符,用map把更新字符出现的次数;

第二次遍历字符,找到第一个map.get(ch)==1的就输出ch,return。

4:求数组中的逆序对数

逆序数在线性代数里面有讲过:数组中前面的数大于后面的数,那么这两个数称为一个逆序对。注意,这里不是只相邻的两数。

传统解法:外层循环逐个遍历数组元素,内层循环从外层循环所指下标开始往数组结尾遍历,统计其后面有多少个值小于外层循环当前所指的元素值,然后加到逆序数对总数里;最后得到的逆序数对总数就是所求。两个循环,复杂度O(n^2)。

优化解法:用归并排序来统计逆序数对。把数组拆分至只含1个元素。然后用归并排序合并子数组,在合并过程中统计逆序对。递归至合并完所有子数组后所统计得到的就是原数组的逆序对数。

int CountPairs(int[] nums){
        if(nums==null){
            return 0;
        }
        //用来存放归并结果的数组
        int[] copy=new int[nums.length];
        for (int i = 0; i < copy.length; i++) {
            copy[i]=nums[i];
        }
        int pairs=MergeCount(nums,copy,0,nums.length-1);
        return pairs;
    }
    int MergeCount(int[] nums,int[] copy,int start,int end){
        //递归到start=end,说明拆分到一个元素一个数组。此时每个数组逆序队为0
        if(start==end){
            copy[start]=nums[start];
            return 0;
        }
        //拆分当前数组
        int half=(end-start)/2;
        //递归获取以中间拆分的左右数组的逆序数对
        int leftCount=MergeCount(nums, copy, start, start+half);
        int rightCount=MergeCount(nums, copy, start+half+1, end);
        //归并,由左右数组合并,统计当前数组的逆序对
        int leftEnd=half;
        int rightEnd=end;
        int mergeindex=end;//指向归并结果数组的末尾,用来接收左右数组中的最大值
        int curr_count=0;
        while(leftEnd>=start && rightEnd>=half+1){
            //由于左右数组都是归并好的有序数组,所以直接用数组最后一个元素进行比较
            if(nums[leftEnd]>nums[rightEnd]){//如果左数组leftEnd值大于右边rightEnd值,
                copy[mergeindex]=nums[leftEnd];//归并,取大者入新数组
                curr_count+=rightEnd-half;//因为右数组有序,并且leftEnd>rightEnd,所以rightStart~rightEnd都是小于leftEnd的,有多少个元素就有多少逆序对
                mergeindex--;
                leftEnd--;
            }else{//否则取右边的为大者,入归并数组
                copy[mergeindex]=nums[rightEnd];
                mergeindex--;
                rightEnd--;
            }            
        }
        //若右数组归并完了而左数组未归并,则把左数组从大到小归并
        for(int i=leftEnd;i>=start;--i){
            copy[mergeindex--]=nums[i];
        }
        //若左数组归并完了而右数组还有,则把右数组从大到小归并
        for(int j=rightEnd;j>=half+1;--j){
            copy[mergeindex--]=nums[j];
        }
        return leftCount+rightCount+curr_count;
    }
 

5:求两个链表的第一个公共结点

传统解法:暴力搜索。遍历链表1的结点,内存循环遍历链表2的链表,直到出现链表1与链表2都指向的同一个结点,说明是公共结点,return。

优化解法:观察发现,两个链表在第一个公共结点处开始合二为一,也就是说两个链表成一个  >—  形。后半段的  —  就是相同的部分,而相同部分的开始点就是两链表的公共点。我们可以用两个辅助栈“从尾到头”比较两链表的结点,因为后面部分是融合的,所以结点是相同的,记录下来作为lastpop,然后出栈。。。。。。直到两栈顶是不同的结点,说明到达了两链表融合之前的结点,那么上一次弹出的lastpop就是第一个公共结点了。

最右解法:对于长短不一的两链表的遍历比较题,用“双指针同步法”。先分别遍历两链表得出各自长度计算出长度差delta,然后长的链表先走delta步,然后在同时遍历两链表以达到同步遍历。当他们首次指向同一个结点时,就是我们要找的第一个公共结点啦!

 

6:数字K在排序数组中出现的次数

传统解法:遍历整个数组,遇到K后统计K出现的次数。

优化解法:因为是排序数组进行查找,第一时间想到二分查找。通过二分查找到一个K的下标后,分别往前、往后统计有多少个K连续即可。

最优解法:上面通过二分查找找到的K不能确定是第一个K还是中间的K还是最后一个K,所以找到后仍需要前后遍历;如果整个数组都是K,那么复杂度和传统解法没差多少。我们知道这是一个有序数组,所以K必定连续出现。既然是连续的,那我们就可以找到第一个K的下标和最后一个K的下标,直接通过下标差就可以得到K的个数了。而在有序数组中求一个数的位置,仍然是二分查找!

public int getFirstK(int[] nums, int start, int end, int k) {
        if (start > end) {
            return -1;
        }
        // 取中间值
        int mid = (start + end) / 2;

        if (nums[mid] == k) {
            // 中间值为K且在数组第一位,则直接返回
            if (mid == 0) {
                return mid;
            }
            //若找到的K不在数组开头并且前一位不是K,则这个就是第一个K
            if (mid > 0 && nums[mid - 1] != k) {
                return mid;
            } else {
                //若前一个也是K,说明第一K在mid的前面,递归这个函数查找start~mid-1这个区间
                end = mid - 1;
            }
            //若中间值大于K,说明K在mid的左面,递归查找start~mid-1范围
        } else if (nums[mid] > k) {
            end = mid - 1;
        } else {
            //若中间值小于K,说明K在mid的右边,查找右边
            start = mid + 1;
        }
        return getFirstK(nums, start, end, k);
    }

    //递归二分查找确定最后一个K,原理同上
    int getLastK(int[] nums, int start, int end, int k) {
        if (start > end) {
            return -1;
        }
        int mid = (start + end) / 2;
        if (mid == nums.length - 1) {
            return mid;
        }
        if (nums[mid] == k) {
            if (mid < nums.length - 1 && nums[mid + 1] != k) {
                return mid;
            } else {
                start = mid + 1;
            }
        } else if (nums[mid] < k) {
            start = mid + 1;
        } else {
            end = mid - 1;
        }
        return getLastK(nums, start, end, k);
    }
    public int Count_K(int[] nums, int k) {
        if (nums == null) {
            return 0;
        }
        //由最后K和第一K的下标差+1得到数组中K的个数
        int first_K = getFirstK(nums, 0, nums.length - 1, k);
        int last_K = getLastK(nums, 0, nums.length - 1, k);
        return last_K - first_K + 1;
    }

7:二叉树的深度

我们用递归的角度来思考这个问题:根的深度=左右子树深度较大值+1,而左右子树的深度也是这条公式递归,直到没有子树则返回0;

public int TreeDepth(MyTreeNode curr_root){
        //递归边界:叶子的子节点为空,返回0
        if(curr_root==null){
            return 0;
        }
        int leftDepth=TreeDepth(curr_root.leftNode);
        int rightDepth=TreeDepth(curr_root.rightNode);
        //当前结点深度等于左右子树深度较大者+1
        return leftDepth>rightDepth?leftDepth+1:rightDepth+1;
    }

8:判断一棵树是否平衡二叉树

 平衡二叉树:任意结点的左右子树的深度差不超过1。

传统方法:由7我们得到了递归求树结点深度的办法,那么我们就可以遍历这棵树的结点,求结点左右子树的深度,然后取差值与1比较即可。缺点:从根开始遍历判断,下面的结点会被计算深度的函数遍历多次。

9:数组中只出现一次的数字

一个数组中有两个数字只出现了一次,其他的都出现了两次。要求找出这两个数字,并且时间复杂度O(n),空间复杂度O(1)。

 我们知道,一个数字异或它自身等于0,一个数异或0等于它自身,连续运算有:a^b^c^d=(a^b)^(c^d)=a^c^b^d...符合交换律。那么对于题目所给数组:除了两个数只出现一次外,其他都出现两次。我们把数组元素取出来做异或,如:a^b^c^d^e^b^a^e=a^a^b^b^e^e^c^d(由交换律)=0^0^0^c^d=c^d。这样我们可以推导出,数组所有元素异或的结果刚好是两个只出现了一次的元素的异或。但是我们要求的是这两个数字而不是他们的异或,如果可以把数组一分为二:每边都只含一个只出现一次的元素,那么分别取异或得到的结果就是两个只出现一次的数字了。于是问题变成了怎么划分数组:我们知道c,d(假设c,d是只出现一次的数字)是不相同的,所以c^d!=0。也就是说c,d的二进制位至少有一位是不同的,我们找到第一个不同的位,记为index,我们就用这位来划分数组:index位为0的在左数组,为1的在右数组。由于两个相同数字各位相同,所以在index位的取值也相同,所以绝对会分配到同一边。而c,d在index位不同,所以会被分到两边。

public int[] fingNumsAppearOnce(int[] nums){
        int[] res=new int[2];
        res[0]=0;
        res[1]=0;
        if(nums==null){
            return null;
        }
        //找到整个数组异或结果的第一个1
        int first_one_index=findOneIndex(nums);
        
        for (int i = 0; i < nums.length; i++) {
            //遍历数组,通过每个元素第first_one_index位是不是1来分配元素在哪边异或
            if(isBitOne(nums[i], first_one_index)){
                res[0]^=nums[i];
            }else{
                res[1]^=nums[i];
            }
        }
        return res;
    }
    //找到整个数组异或结果的从右往左第一个1下标
    int findOneIndex(int[] nums){
        int res=0;
        for(int i:nums){
            res=res^i;
        }
        int index=0;
        while((res&1)!=1){
            res>>=1;
            index++;
        }
        return index;
    }
    //判断右起第index位是不是1
    boolean isBitOne(int num,int index){
        num>>=index;
        return (num&1)==1;
    }

10:在增序数组中找到一对数字,其和为S。

因为是递增数组,我们采取“双下标迫近法”——一个下标指向开头,一个指向结尾,计算两下标所指元素的和,大于S则右指针左移,小于S则左指针右移动,逐步迫近S最终得到和为S的两元素。

11:给定一个数S,打印出所有和为S的连续正序列,要求序列至少包含两个数。

比如:15=7+8=4+5+6=1+2+3+4+5

我们同样采用双数迫近法——令start=1,end=2,此时和sum为3.令half=(S+1)/2,在start<=half时不断改变start、end的值去寻找和为S的序列:

sum=start+(start+1)+...+end,所以当end右移时增大范围,sum增大,start右移时缩小范围,sum减小,以此操作序列的变化。

当sum=S时,此时start~end为一个和为S的序列,打印输出,然后end++增大sum继续寻找;

当sum<S时,end++增大sum再与S比较;

当sum>S时,start++缩小范围减小sum,再与S比较;

...

直到start=(S+1)/2,此时end最小为(S+1)/2+1,start+end>=S了,再增大也是大于S,没必要再增大去找了,循环停止。

 

转载于:https://www.cnblogs.com/ygj0930/p/6434201.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值