剑指offer之每日6题 ----- 第五天

原题连接

1.复杂链表的复制

题目描述

输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的head。(注意,输出结果中请不要返回参数中的节点引用,否则判题程序会直接返回空)

思路

直观解法就是第一步复制链表的每个节点,用p_next连接,第二步连接特殊指针的值random,首先要找到这个random的值,因为是指向任意的一个节点,所以就需要再次遍历整个链表,那么定位每个节点的random值,需要o(n)步,那么n个节点的时间复杂度就是o(n^2)。

第二种就是以空间换时间,用map集合来映射原结点和复制的节点,第一步还是复制原始链表的每个节点,第二步就是复制每个节点上的random节点,这样可以用o(1)时间来找到对应的random节点,那么n个节点的时间复杂度就是o(n),空间复杂度也是o(n)

第三种,在不用辅助空间情况下,实现o(n)的时间效率。
第一步复制每个节点,将每个复制之后的节点插入到原结点的后面。
第二步设置复制出来的random节点,就是原结点的random指针的next节点
第三步拆分链表,奇数位置的就是原链表,偶数位置的就是复制的链表

  public RandomListNode Clone(RandomListNode pHead) {
         if (pHead == null) {
            return null;
        }
        RandomListNode currentNode = pHead;
        //1.复制原结点
        while (currentNode != null) {

            RandomListNode cloneNode = new RandomListNode(currentNode.label);
            RandomListNode nextNode = currentNode.next;
            currentNode.next = cloneNode;
            cloneNode.next = nextNode;
            currentNode = nextNode;
        }
        //2.复制随机节点
        currentNode = pHead;
        while (currentNode != null) {
            currentNode.next.random = currentNode.random == null ? null : currentNode.random.next;
            currentNode = currentNode.next.next;
        }
        //3.拆分链表
        currentNode = pHead;
        RandomListNode cloneHead = pHead.next;
        while (currentNode != null) {
            RandomListNode cloneNode = currentNode.next;
            currentNode.next = cloneNode.next;
            cloneNode.next = cloneNode.next == null ? null : cloneNode.next.next;
            currentNode = currentNode.next;
        }
        return cloneHead;
    }

2.二叉搜索树与双向链表

题目描述

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

思路

转换成一个排序的双向链表,而二叉搜索树的中序遍历就是按照从小到大的顺序遍历二叉树的,所以这里可以使用中序遍历,双向链表,可以将原先指向左子节点的指针调整为链表中指向前一个指针的节点,右子节点为链表中指向后一个节点的指针,当访问到根节点时候,前一个指针指向左子树上最大值,后一个指针指向右子树最小值,采用递归做法

public class Solution {
        protected TreeNode pLast = null;
        public TreeNode Convert(TreeNode pRootOfTree) {
              if (pRootOfTree == null)
                return null;

            if (pRootOfTree.left == null && pRootOfTree.right == null) {
                pLast = pRootOfTree;
                return pRootOfTree;
            }

            //构建左子树双向链表
            TreeNode left = Convert(pRootOfTree.left);
            //左子树不为空,将左子树添加为根节点的前一指针
            if (left != null) {
                pLast.right = pRootOfTree;
                pRootOfTree.left = pLast;
            }
            pLast = pRootOfTree;
            //构造右子树双向链表
            TreeNode right = Convert(pRootOfTree.right);
            //如果右子树不为空,根节点的后一指针指向右子树的最小值
            if (right != null) {
                right.left = pRootOfTree;
                pRootOfTree.right = right;
            }
            //返回双向链表的头节点
            return left != null ? left : pRootOfTree;
        }

3.字符串的排列

题目描述

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

思路

这个在数学上应该是叫做排列组合的全排列,在算法中也是典型的递归,可以先找出第一个位置的字符,然后与后面的字符进行交换,第二步固定第一个字符。
要注意这里的字符有可能重复,所以要考虑到去重,也就是在交换的时候,从第一个数起,要与后面非重复的值交换。
采用了回溯法思想
在这里插入图片描述

 public ArrayList<String> Permutation(String str) {
        ArrayList<String> res = new ArrayList<>();
        if (str !=null && (str.length() > 0)){
            Permutation(str.toCharArray(),0,res);
            Collections.sort(res);
        }
        return res;
    }
    private void Permutation(char[] chars, int i, ArrayList<String> res) {
        if (i == chars.length-1){
            res.add(String.valueOf(chars));
        }else{
            Set<Character> charSet = new HashSet<>();
            for (int j = i; j < chars.length; j++) {
                //去重
                if (j==i || !charSet.contains(chars[j])){
                    charSet.add(chars[j]);
                    swap(chars,i,j);
                    Permutation(chars,i+1,res);
                    swap(chars,j,i);
                }
            }
        }
    }
	//交换数字
    private void swap(char[] chars, int i, int j) {
        char temp =chars[i];
        chars[i] =chars[j];
        chars[j] =temp;
    }

4.数组中出现次数超过一半的数字

题目描述

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

思路

解法一是如果这个数字出现的次数超过数组长度的一半,那么这个数字以为是位于数组中间的那个数,可以对数组进行排序,求出中间那个数,可以使用快排,这样时间复杂度是o(nlogn)。
解法二是如果数字符合要求,那么这个数字出现的次数一定比数组中其他数字出现的次数的和还要多,那么可以在遍历数组的时候,设置两个值,一个用来保存数字,一个用来保存次数,遍历下一个数字的时候,如果相等,那么次数加1,如果不相等,那么次数减1,如果次数为0 ,则保存下一个数字,这样最后的值一定是次数超过一半的,这个解法的时间复杂度是o(n);

 public int MoreThanHalfNum_Solution(int[] array) {
        if (array.length == 0)
            return 0;
        //遍历数组记录次数
        int result = array[0];
        int times = 1;
        for (int i = 0; i < array.length; i++) {
            if (times == 0) {
                result = array[i];
                times = 1;
            } else if (result == array[i])
                ++times;
            else --times;
        }
        times = 0;
        //判断result是否符合条件
        for (int i = 0; i < array.length; i++) {
            if (array[i] == result)
                ++times;
        }

        return (times > array.length / 2) ? result : 0;
    }

5. 最小K个数

题目描述

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

思路

直观解法就是快速排序然后就可以得到,这样的解法时间复杂度是o(nlogn);
第二种思路就是可以创建一个大小为k的数据容器来存储最小的k个数,第一步在k个整数中,找到最大值,第二步如果从n个整数中读入的数字大于这个最大值,就将最大值删除,第三步将这个数字插入进容器中。每次都要k个数字的最大值,可以采用最大堆,也可以采用红黑树。

 public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
        ArrayList<Integer> list =new ArrayList<>();
        //检查特殊情况
        if (input == null || input.length <=0 || input.length <k){
            return list;
        }
        //构建最大堆,二叉堆
        //使用数组表示二叉堆,其左孩子就是父节点下标的2倍,右孩子就是父节点下标的2倍+1
        for (int len = k/2-1; len >= 0; len--) {
            adjustMaxHeap(input,len,k-1);
        }
        //从第k个元素开始比较,如果比最大值小,则替换并调整
        int tmp;
        for (int i = k; i < input.length; i++) {
            if (input[i] < input[0]){
                tmp =input[i];
                input[i]=input[0];
                input[0]=tmp;
                adjustMaxHeap(input,0,k-1);
            }
        }
        for (int j = 0; j < k; j++) {
            list.add(input[j]);
        }
       return list;
    }

    private void adjustMaxHeap(int[] input, int pos, int length) {
        //大顶堆,是二叉树的一种表现形式,其根节点比其他节点值都大
        int temp ;
        int child;
        for (temp = input[pos];2 * pos+1 <= length ;pos = child){
            child = 2* pos+1;
            if (child < length && input[child] < input[child+1]){
                child++;
            }
            if (input[child] > temp){
                input[pos]=input[child];
            }else{
                break;
            }
        }
        input[pos]=temp;
    }

6.连续子数组的最大和

题目描述

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

思路

蛮力法,时间复杂度为o(n^2)
动态规划法:当以第i-1个数字结尾的子数组中所有数字的和小于0时,这时候如果把这个负数和第i个数字相加,一定是小于第i个数字本身,所以这种情况下最大和是这个i数字本身

 public int FindGreatestSumOfSubArray(int[] array) {
         if (array == null || array.length == 0)
            return 0;
        int sum = array[0];
        int tempSum = array[0];

        for (int i = 1; i < array.length; i++) {
            tempSum = (tempSum < 0) ? array[i] : tempSum + array[i];
            sum = (tempSum > sum) ? tempSum : sum;
        }
        
        return sum;
    }

猜你喜欢

转载自blog.csdn.net/YuQing_Cat/article/details/86295241