第八章数组和矩阵问题

1,转圈打印矩阵
题目:给定一个矩阵,请按照转圈打印的方式打印它。
例如:
    1   2   3   4
    5   6   7   8
    9   10  11  12
    13  14  15  16
转圈打印结果:1,2,3,4,8,12,16,15,14,13,9,5,6,7,11,10。
要求:时间复杂度O(1)。
解答:矩阵分圈处理,在矩阵式利用左上角的坐标(tR,tC)和右下角的坐标(dR,dC)就可以表示一个子矩阵,比如,题目中的矩阵,当(tR,tC)=(0,0)、(dR,dC)=(3,3)时,表示的矩阵就是整个矩阵,那么这个字矩阵最外层的部分就是:
    1   2   3   4
    5           8
    9           12  
    13  14  15  16
这就是当(tR,tC)=(0,0)、(dR,dC)=(3,3)时的子矩阵。接下来令tR和tC加1,即(tR,tC)=(1,1),令dR和dC减1,即(dR,dC)=(2,2),此时表示的子矩阵如下:
    6   7
    10  11
再转圈打印这个矩阵,tR和tC加1,dR和dC减1,如果发现左上角坐标跑到了右下角坐标的右方或下方,整个过程就停止。
参看如下的代码,spiralOrderPrint方法,其中printEdge就是转圈打印一个子矩阵的外层。
    public static void spiralOrderPrint(int[][] matrix) {
        int tR = 0;
        int tC = 0;
        int dR = matrix.length - 1;
        int dC = matrix[0].length - 1;
        while (tR <= dR && tC <= dC) {
            printEdge(matrix, tR++, tC++, dR--, dC--);
        }
    }

    public static void printEdge(int[][] m, int tR, int tC, int dR, int dC) {
        if (tR == dR) { // �Ӿ���ֻ��һ��ʱ
            for (int i = tC; i <= dC; i++) {
                System.out.print(m[tR][i] + " ");
            }
        } else if (tC == dC) { // �Ӿ���ֻ��һ��ʱ
            for (int i = tR; i <= dR; i++) {
                System.out.print(m[i][tC] + " ");
            }
        } else { // һ�����
            int curC = tC;
            int curR = tR;
            while (curC != dC) {
                System.out.print(m[tR][curC] + " ");
                curC++;
            }
            while (curR != dR) {
                System.out.print(m[curR][dC] + " ");
                curR++;
            }
            while (curC != tC) {
                System.out.print(m[dR][curC] + " ");
                curC--;
            }
            while (curR != tR) {
                System.out.print(m[curR][tC] + " ");
                curR--;
            }
        }
    }
2,将正方形矩阵顺时针转动90度
题目:将一个n*n矩阵顺时针转90度,例如
    1   2   3   4
    5   6   7   8
    9   10  11  12
    13  14  15  16
顺时针转90度后的矩阵
    13  9   5   1
    14  10  6   2
    15  11  7   3
    16  12  8   4
要求:额外空间复杂度为O(1)。
这里仍然使用分圈打印的方式,在矩阵的左上角和右下角的坐标就可以表示一个矩阵,比如,题目中的矩阵,当(tR,tC)=(0,0)、(dR,dC)=(3,3)时,表示的子矩阵就是整个矩阵,那么这个子矩阵最外层的部分如下:
1   2   3   4
5           8
9           12
13  14  15  16
在这个外圈中,1,4,16,13为一组,然后让1占据4的位置,4占据16的位置,16占据13的位置,13占据1的位置,一组就调整完了。然后2,8,15,9,继续占据调整的过程,最后3,12 ,14 ,5为一组,继续占据调整的过程。然后(tC,tC)、(dR,dC)=(3,3)的子矩阵外层就调整完毕。接下来令tR和tC加1,即(tR,tC)=(1,1),令dR和dC减1,即(dR,dC)=(2,2),此时表示的矩阵如下
6   7
10  11
这个外层只有一组,就是6,7,10,11,占据调整之后即可,所以如果子矩阵的大小是m*m,一共就有m-1组,分别进行占据调整即可。
请参考如下代码中的rotate方法。
    public static void rotate(int[][] matrix) {
        int tR = 0;
        int tC = 0;
        int dR = matrix.length - 1;
        int dC = matrix[0].length - 1;
        while (tR < dR) {
            rotateEdge(matrix, tR++, tC++, dR--, dC--);
        }
    }

    public static void rotateEdge(int[][] m, int tR, int tC, int dR, int dC) {
        int times = dC - tC; 
        int tmp = 0;
        for (int i = 0; i != times; i++) { 
            tmp = m[tR][tC + i];
            m[tR][tC + i] = m[dR - i][tC];
            m[dR - i][tC] = m[dR][dC - i];
            m[dR][dC - i] = m[tR + i][dC];
            m[tR + i][dC] = tmp;
        }
    }
3,“之”字型打印矩阵
题目:给定一个矩阵,按照“之”字型打印矩阵,例如:
1   2   3   4
5   6   7   8
9   10  11  12
13  14  15  16
"之"字型打印矩阵的结果为:1,2,5,9,6,3,4,7,10,13,14,11,8,12,15,16。
    public static void printMatrixZigZag(int[][] matrix) {
        int tR = 0;
        int tC = 0;
        int dR = 0;
        int dC = 0;
        int endR = matrix.length - 1;
        int endC = matrix[0].length - 1;
        boolean fromUp = false;
        while (tR != endR + 1) {
            printLevel(matrix, tR, tC, dR, dC, fromUp);
            tR = tC == endC ? tR + 1 : tR;
            tC = tC == endC ? tC : tC + 1;
            dC = dR == endR ? dC + 1 : dC;
            dR = dR == endR ? dR : dR + 1;
            fromUp = !fromUp;
        }
        System.out.println();
    }

    public static void printLevel(int[][] m, int tR, int tC, int dR, int dC,
            boolean f) {
        if (f) {
            while (tR != dR + 1) {
                System.out.print(m[tR++][tC--] + " ");
            }
        } else {
            while (dR != tR - 1) {
                System.out.print(m[dR--][dC++] + " ");
            }
        }
    }
4,找到无序数组中最小的k个数
题目:给定一个无序的整型数组arr,找到其中最小的k个数。
要求:数组长度为n,排序之后自然可以得到最小的k个数,此时时间复杂度与排序的时间复杂度相同,均为O(logn)。本体要求读者实现时间复杂度为O(nlogk)和O(n)的方法。
O(nlogk)时间复杂度的方法:用堆结构解决,已知维护一个有k个元素的大根堆,这个堆代表目前选出的k个最小的数,在堆里的k个元素中堆顶的元素是最小的k个数里最大的那个。接下里遍历数组,遍历的过程中看当前数是否比堆定元素小,若是,就把堆顶的元素替换成当前的数,然后从堆定的位置调整整个堆,让替换操作后的最大元素继续处在堆定的位置;若不是,则不进行任何操作,继续遍历下一个数;在遍历完成后,堆中的k个数就是所有数组中最小的k个数。
参看下面的getMinKNumsByHeap方法,代码中的heapinsert和heapfy方法分别为堆排序中的建堆和调整堆的实现。
// O(N*logK)
    public static int[] getMinKNumsByHeap(int[] arr, int k) {
        if (k < 1 || k > arr.length) {
            return arr;
        }
        int[] kHeap = new int[k];
        for (int i = 0; i != k; i++) {
            heapInsert(kHeap, arr[i], i);
        }
        for (int i = k; i != arr.length; i++) {
            if (arr[i] < kHeap[0]) {
                kHeap[0] = arr[i];
                heapify(kHeap, 0, k);
            }
        }
        return kHeap;
    }

    public static void heapInsert(int[] arr, int value, int index) {
        arr[index] = value;
        while (index != 0) {
            int parent = (index - 1) / 2;
            if (arr[parent] < arr[index]) {
                swap(arr, parent, index);
                index = parent;
            } else {
                break;
            }
        }
    }

    public static void heapify(int[] arr, int index, int heapSize) {
        int left = index * 2 + 1;
        int right = index * 2 + 2;
        int largest = index;
        while (left < heapSize) {
            if (arr[left] > arr[index]) {
                largest = left;
            }
            if (right < heapSize && arr[right] > arr[largest]) {
                largest = right;
            }
            if (largest != index) {
                swap(arr, largest, index);
            } else {
                break;
            }
            index = largest;
            left = index * 2 + 1;
            right = index * 2 + 2;
        }
    }
    public static void swap(int[] arr, int index1, int index2) {
        int tmp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = tmp;
    }
O(n)的算法,会用到经典的bfprt算法,该算法于1973年由blum、floyd、pratt、rivest、tarjan联合发明,它解决了这样一个问题,在O(n)时间复杂度内,从无序的数组中找到第k小的数,显而易见的是,如果我们找到了第k小的数,那么想求arr中最小的k个数,就是再遍历一次数组的工作量而已,所以关键的问题就变成了如何理解并实现brprt算法。
bfprt算法是如何找到第k小的数?以下是算法的过程,假设bfprt算法的函数是int select(int[]arr,k),该函数的功能为在arr中找到第k小的数,然后返回该数。
select(arr,k)的过程如下:
1),将arr中的n个元素划分成n/5组,每组5个元素,如果最后的组不够5个元素,那么最后的元素成为一组(n%5个元素)。
2),对每个数组进行插入排序,只针对每个组最多5个元素之间的组内排序,组与组之间并不排序。排序之后找到每个组的中位数,如果组的元素个数为偶数,这里规定找不到中位数。
3),步骤2中一共会找到n/5个中位数,让这些中位数组成一个新的数组,记为mArr。递归调用select(mArr,mArr.length/2),意义是找到mArr这个数组中的中位数,即mArr中的第(mArr.length/2)小的数。
4),假设步骤3递归调用select(mArr,mArr.length/2)后,返回的数为x,根据这个x划分整个arr数组(partition过程),划分的过程为:在arr中,比x小的数都在x的左边,比x大的数都在x的右边,x在中间。假划分完成后,x在arr中的位置记为i。
5),如果i==k,说明x为整个数组中第k小的数,直接返回。
    如果i<k,说明x处在第k小的数的左边,应该在x的右边寻找第k小的数,所以递归调用select函数,在右半区寻找第k-i小的数。
    如果i>k,说明x处在第k小的数的右边,应该在x的左边寻找第k小的数,所以递归调用select函数,在左半区寻找第k-i小的数。

bfprt算法为什么能做到O(n)的时间复杂度呢?以下是bfprt时间复杂度的分析:
1,在上述过程,除了步骤3和步骤5要用递归函数外,其他的所有处理过程都可以在O(n)时间内完成。
2,步骤3中有递归函数的调用,且递归处理的数组大小为n/5(即T(n/5))。
3,步骤5也递归调用了select函数,那么递归处理的数组大小最大为多少呢?具体地说,我们关心的是由x划分出的左半区最大有多大和由x划分出去的右半区最大有多大。
数学证明了bfprt算法的时间复杂度是O(n)。
具体请参看如下代码中的getMinKnumsByBFPRT方法。
    public static int[] getMinKNumsByBFPRT(int[] arr, int k) {
        if (k < 1 || k > arr.length) {
            return arr;
        }
        int minKth = getMinKthByBFPRT(arr, k);
        int[] res = new int[k];
        int index = 0;
        for (int i = 0; i != arr.length; i++) {
            if (arr[i] < minKth) {
                res[index++] = arr[i];
            }
        }
        for (; index != res.length; index++) {
            res[index] = minKth;
        }
        return res;
    }

    public static int getMinKthByBFPRT(int[] arr, int K) {
        int[] copyArr = copyArray(arr);
        return select(copyArr, 0, copyArr.length - 1, K - 1);
    }

    public static int[] copyArray(int[] arr) {
        int[] res = new int[arr.length];
        for (int i = 0; i != res.length; i++) {
            res[i] = arr[i];
        }
        return res;
    }

    public static int select(int[] arr, int begin, int end, int i) {
        if (begin == end) {
            return arr[begin];
        }
        int pivot = medianOfMedians(arr, begin, end);
        int[] pivotRange = partition(arr, begin, end, pivot);
        if (i >= pivotRange[0] && i <= pivotRange[1]) {
            return arr[i];
        } else if (i < pivotRange[0]) {
            return select(arr, begin, pivotRange[0] - 1, i);
        } else {
            return select(arr, pivotRange[1] + 1, end, i);
        }
    }

    public static int medianOfMedians(int[] arr, int begin, int end) {
        int num = end - begin + 1;
        int offset = num % 5 == 0 ? 0 : 1;
        int[] mArr = new int[num / 5 + offset];
        for (int i = 0; i < mArr.length; i++) {
            int beginI = begin + i * 5;
            int endI = beginI + 4;
            mArr[i] = getMedian(arr, beginI, Math.min(end, endI));
        }
        return select(mArr, 0, mArr.length - 1, mArr.length / 2);
    }

    public static int[] partition(int[] arr, int begin, int end, int pivotValue) {
        int small = begin - 1;
        int cur = begin;
        int big = end + 1;
        while (cur != big) {
            if (arr[cur] < pivotValue) {
                swap(arr, ++small, cur++);
            } else if (arr[cur] > pivotValue) {
                swap(arr, cur, --big);
            } else {
                cur++;
            }
        }
        int[] range = new int[2];
        range[0] = small + 1;
        range[1] = big - 1;
        return range;
    }

    public static int getMedian(int[] arr, int begin, int end) {
        insertionSort(arr, begin, end);
        int sum = end + begin;
        int mid = (sum / 2) + (sum % 2);
        return arr[mid];
    }

    public static void insertionSort(int[] arr, int begin, int end) {
        for (int i = begin + 1; i != end + 1; i++) {
            for (int j = i; j != begin; j--) {
                if (arr[j - 1] > arr[j]) {
                    swap(arr, j - 1, j);
                } else {
                    break;
                }
            }
        }
    }

    public static void swap(int[] arr, int index1, int index2) {
        int tmp = arr[index1];
        arr[index1] = arr[index2];
        arr[index2] = tmp;
    }

    public static void printArray(int[] arr) {
        for (int i = 0; i != arr.length; i++) {
            System.out.print(arr[i] + " ");
        }
        System.out.println();
    }
5,需要排序的最短子数组长度
题目:一个无序数组arr,求出需要排序的最短子数组长度。
例如:arr={1,5,3,4,2,6},返回4,因为只有[5,3,4,2]需要排序。
解答:可以做到时间复杂度O(n),额外空间复杂度O(1)。
初始化变量noMinIndex=-1,从右向左遍历,遍历的过程中记录右侧出现过的数的最小值,记为min。假设当前数为arr[i],如果arr[i]>min,说明如果要整体有序,min值必然会挪到arr[i]的左边。用noMinIndex记录最左边出现这种情况的位置。如果遍历完成后,noMinIndex依然等于-1,说明从右到左始终不升序,原本数组就有序,直接返回0,即完全不需要排序。
接下来从左往右遍历,遍历的过程中记录左侧出现过的数的最大值,记为max。假设当前数为arr[i],如果arr[i]<max,说明如果有序,max值必然会挪到arr[i]的右边。用变量noMaxIndex记录最右边出现这种情况的位置。
遍历完成后,arr[noMinIndex...noMaxIndex]就是需要排序的那部分,返回它的长度即可。
具体过程参看下面的getMinLength方法。
    public static int getMinLength(int[] arr) {
        if (arr == null || arr.length < 2) {
            return 0;
        }
        int min = arr[arr.length - 1];
        int noMinIndex = -1;
        for (int i = arr.length - 2; i != -1; i--) {
            if (arr[i] > min) {
                noMinIndex = i;
            } else {
                min = Math.min(min, arr[i]);
            }
        }
        if (noMinIndex == -1) {
            return 0;
        }
        int max = arr[0];
        int noMaxIndex = -1;
        for (int i = 1; i != arr.length; i++) {
            if (arr[i] < max) {
                noMaxIndex = i;
            } else {
                max = Math.max(max, arr[i]);
            }
        }
        return noMaxIndex - noMinIndex + 1;
    }
6,在数组中找到出现次数大于n/k的数
题目:给定一个数组,打印其出现次数大于一半的数,如果没有这样的数,打印提示信息。
进阶:给定一个数组arr,再给定一个整数k,打印所有出现次数大于n/k的数,如果没有这样的数,打印提示信息。
要求:原问题要求的时间复杂度为O(n),额外空间复杂度为O(1),进阶问题要求时间复杂度为O(n*k),额外空间复杂度为O(k)。
解答:无论是原问题还是进阶问题,都可以用哈希表记录每个数及其出现的次数,但是额外空间复杂度为O(n),不符合题目要求,所以本书不再详细讲这种方法。本书提供方法的核心思路是,一次在数组中删除k个不同的数,不停地删除,直到剩下数的种类不足k就停止删除,那么,如果一个数在数组中出现的次数大于n/k,则这个数最后一定会被生下来。
原问题,出现次数大于一半的数最多只会有一个,还可能不存在这样的数。具体的过程为,一次在数组中删除两个不的数,不停地删除,直到剩下的数只有一种,如果一个数出现次数大于一半,这个数最后一定会剩下来。如下代码中的printHalfMajor方法。
    public static void printHalfMajor(int[] arr) {
        int cand = 0;
        int times = 0;
        for (int i = 0; i != arr.length; i++) {
            if (times == 0) {
                cand = arr[i];
                times = 1;
            } else if (arr[i] == cand) {
                times++;
            } else {
                times--;
            }
        }
        times = 0;
        for (int i = 0; i != arr.length; i++) {
            if (arr[i] == cand) {
                times++;
            }
        }
        if (times > arr.length / 2) {
            System.out.println(cand);
        } else {
            System.out.println("no such number.");
        }
    }
第一个for循环就是一次在数组中删除掉两个不同的数的代码实现。
把cand变量叫候选,times叫作次数,读者先不用纠结这两个变量是什么意义,我们看在第一个for循环中发生了什么。
    times=0时,表示当前没有候选,则把当前数arr[i]设为候选,同时把times设置成1.
    times!=0时,表示当前有候选,如果当前的数arr[i]与候选一样,同时把times加1,如果当前的数att[i]与候选不一样,就把times减1,减到0则表示又没有候选了。
进阶问题也是类似的思想,一次在数组中删除k个不同的数,不停地删除,直到剩下的数的种类不足k,那么,如果某些数在数组中出现次数大于n/k,则这些数最后一定会剩下来。
具体参看如下的printKMajor方法。
    public static void printKMajor(int[] arr, int K) {
        if (K < 2) {
            System.out.println("the value of K is invalid.");
            return;
        }
        HashMap<Integer, Integer> cands = new HashMap<Integer, Integer>();
        for (int i = 0; i != arr.length; i++) {
            if (cands.containsKey(arr[i])) {
                cands.put(arr[i], cands.get(arr[i]) + 1);
            } else {
                if (cands.size() == K - 1) {
                    allCandsMinusOne(cands);
                } else {
                    cands.put(arr[i], 1);
                }
            }
        }
        HashMap<Integer, Integer> reals = getReals(arr, cands);
        boolean hasPrint = false;
        for (Entry<Integer, Integer> set : cands.entrySet()) {
            Integer key = set.getKey();
            if (reals.get(key) > arr.length / K) {
                hasPrint = true;
                System.out.print(key + " ");
            }
        }
        System.out.println(hasPrint ? "" : "no such number.");
    }

    public static void allCandsMinusOne(HashMap<Integer, Integer> map) {
        List<Integer> removeList = new LinkedList<Integer>();
        for (Entry<Integer, Integer> set : map.entrySet()) {
            Integer key = set.getKey();
            Integer value = set.getValue();
            if (value == 1) {
                removeList.add(key);
            }
            map.put(key, value - 1);
        }
        for (Integer removeKey : removeList) {
            map.remove(removeKey);
        }
    }

    public static HashMap<Integer, Integer> getReals(int[] arr,
            HashMap<Integer, Integer> cands) {
        HashMap<Integer, Integer> reals = new HashMap<Integer, Integer>();
        for (int i = 0; i != arr.length; i++) {
            int curNum = arr[i];
            if (cands.containsKey(curNum)) {
                if (reals.containsKey(curNum)) {
                    reals.put(curNum, reals.get(curNum) + 1);
                } else {
                    reals.put(curNum, 1);
                }
            }
        }
        return reals;
    }

猜你喜欢

转载自blog.csdn.net/u010075989/article/details/81129443