java算法学习索引之数组矩阵问题

一 将正方形矩阵顺时针转动90°

给定一个N×N的矩阵matrix,把这个矩阵调整成顺时针转动90°后的形式。

顺时针转动90°后为:

【要求】额外空间复杂度为O(1)。

public 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--); // 旋转每一层
    }
}

private void rotateEdge(int[][] matrix, int tR, int tC, int dR, int dC) {
    int times = dC - tC; // 当前层的元素个数
    int tmp = 0; // 用于交换的中间变量
    for (int i = 0; i < times; i++) { // 循环遍历当前层的元素
        tmp = matrix[dR][tC + i];
        matrix[tR][tC + i] = matrix[dR - i][tC];
        matrix[dR - i][tC] = matrix[dR][dC - i];
        matrix[dR][dC - i] = matrix[tR + i][dC];
        matrix[tR + i][dC] = tmp;
    }
}

这段代码实现了一个将二维数组(矩阵)顺时针旋转90度的方法,采用边界逐层旋转的方式。

rotate() 方法中,使用四个变量来记录矩阵的左上角坐标和右下角坐标,并在 while 循环中不断收缩边界,对每一层使用 rotateEdge() 方法来旋转。

rotateEdge() 方法是旋转操作的核心,其中 times 记录当前层的元素个数,tmp 用于保存当前要旋转的元素,通过四个 for 循环分别完成旋转操作,具体如下:

  1. 将矩阵的右上角元素 m[dR][tC+i] 赋值给左上角元素 m[tR][tC+i]

  2. 将矩阵的左下角元素 m[dR-i][tC] 赋值给右上角元素 m[tR][tC+i]

  3. 将矩阵的左上角元素 m[dR][tC-i] 赋值给左下角元素 m[dR-i][tC]

  4. 将 tmp 值赋值给右下角元素 m[tR+i][dC]

在 rotateEdge() 方法中,使用 for 循环遍历每一层元素,i 从 0 开始,每次循环收缩一圈,直到完成该层所有元素的旋转。

二 “之”字形打印矩阵

【题目】给定一个矩阵matrix,按照“之”字形的方式打印这个矩阵,例如:

“之”字形打印的结果为:1,2,5,9,6,3,4,7,10,11,8,12。

【要求】额外空间复杂度为O(1)。

要按照"之"字形的方式打印矩阵,我们可以根据打印方向分为两种情况:从左上到右下以及从右下到左上。我们可以使用两个变量来表示当前打印的行和列,根据当前行和列的奇偶性来确定打印顺序。

下面是按照"之"字形打印矩阵的 Java 代码实现:

public void printZMatrix(int[][] matrix) {
    if (matrix == null || matrix.length == 0) {
        return;
    }
    int row = matrix.length; // 矩阵行数
    int col = matrix[0].length; // 矩阵列数
    int tR = 0; // 左上角行坐标
    int tC = 0; // 左上角列坐标
    int dR = 0; // 右下角行坐标
    int dC = 0; // 右下角列坐标
    boolean fromUp = false; // 打印方向标志位,false 表示从右下到左上,true 表示从左上到右下

    while (tR < row) {
        printLevel(matrix, tR, tC, dR, dC, fromUp);

        // 更新左上角和右下角的坐标
        tR = tC == col - 1 ? tR + 1 : tR; // 如果当前列已经到达最后一列,则向下移动
        tC = tC == col - 1 ? tC : tC + 1; // 如果当前列已经到达最后一列,则不再向右移动
        dC = dR == row - 1 ? dC + 1 : dC; // 如果当前行已经到达最后一行,则向右移动
        dR = dR == row - 1 ? dR : dR + 1; // 如果当前行已经到达最后一行,则不再向下移动
        fromUp = !fromUp; // 改变打印方向
    }
}

private void printLevel(int[][] matrix, int tR, int tC, int dR, int dC, boolean fromUp) {
    if (fromUp) { // 从左上到右下打印
        while (tR <= dR && tC >= dC) {
            System.out.print(matrix[tR++][tC--] + " ");
        }
    } else { // 从右下到左上打印
        while (dR >= tR && dC <= tC) {
            System.out.print(matrix[dR--][dC++] + " ");
        }
    }
}


  
以上代码中,printZMatrix() 方法用于控制行和列的遍历,根据打印的方向调用 printLevel() 方法来打印每一层的元素。

printLevel() 方法根据打印方向,使用 while 循环来打印每一层的元素,从左上到右下打印时,行坐标递增,列坐标递减;从右下到左上打印时,行坐标递减,列坐标递增。

三  找到无序数组中最小的k个数

【题目】给定一个无序的整型数组arr,找到其中最小的k个数。

【要求】如果数组arr的长度为N,排序之后自然可以得到最小的k个数,此时时间复杂度与排序的时间复杂度相同,均为O(NlogN)。本题要求实现时间复杂度为O(Nlogk)和O(N)的方法。

快速排序实现


import java.util.Arrays;

class QuickSort {
    public static void quickSort(int[] arr, int left, int right) {
        if (left < right) {
            // 划分数组并获取基准值的索引
            int pivotIndex = partition(arr, left, right);
            // 对基准值左右两侧的子数组进行递归排序
            quickSort(arr, left, pivotIndex - 1);
            quickSort(arr, pivotIndex + 1, right);
        }
    }

    public static int partition(int[] arr, int left, int right) {
        // 选择最右边的元素作为基准值
        int pivot = arr[right];
        int i = left - 1; // i 表示小于基准值的元素的最右边界索引
        for (int j = left; j < right; j++) {
            if (arr[j] < pivot) {
                // 将当前元素交换到小于基准值的部分
                i++;
                swap(arr, i, j);
            }
        }
        // 将基准值交换到适当的位置
        swap(arr, i + 1, right);
        return i + 1;
    }

    public static void swap(int[] arr, int i, int j) {
        // 交换数组中的两个元素
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }
}

public class Main {
    public static void main(String[] args) {
        int[] arr = {10, 7, 8, 9, 1, 5};
        int k = 3;
        // 使用快速排序对数组进行排序
        QuickSort.quickSort(arr, 0, arr.length - 1);
        // 复制排序后的前 k 个元素到新数组中
        int[] smallestK = Arrays.copyOf(arr, k);
        // 打印输出最小的 k 个数字
        System.out.println(Arrays.toString(smallestK));
    }
}


在上述代码中,添加了详细的注释来解释每个步骤的作用和实现原理。这些注释可以帮助你更好地理解代码的逻辑和功能。运行结果不变,仍会输出 [1, 5, 7],即最小的 3 个数字。

用堆的方法

public int[] getMinKNumsByHeap(int[] arr,int k) {
    // 如果k的值不合法,直接返回原数组
    if (k < 1 || k > arr.length) {
        return arr;
    }

    // 创建一个大小为k的最大堆数组用于保存最小的k个数
    int[] kHeap = new int[k];
    for (int i = 0; i < k; i++) {
        // 利用heapInsert方法将前k个数构建成最大堆
        heapInsert(kHeap, arr[i], i);
    }

    // 对于后续的数,如果比堆中的最大值(kHeap[0])小,则替换并重新调整堆
    for (int i = k; i < arr.length; i++) {
        if (arr[i] < kHeap[0]) {
            kHeap[0] = arr[i];
            heapify(kHeap, 0, k);
        }
    }

    return kHeap;
}

// 向堆中插入一个元素,并调整堆结构使其满足最大堆性质
private 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;
        }
    }
}

// 调整堆结构,使其满足最大堆性质
private 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 a, int b) {
    int temp = arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
}

这段代码实现了使用BFPRT算法获取数组中最小的k个数。


public int[] getMinKNumsByBFPRT(int[] arr, int k) {
    if (k < 1 || k > arr.length) {
        return arr;
    }
    int[] res = new int[k];
    int minKth = getMinKthByBFPRT(arr, k); // 获取第k小的数
    int index = 0;
    for (int i = 0; i != arr.length; i++) {
        if (arr[i] < minKth) {
            res[index++] = arr[i]; // 将小于minKth的数放入结果数组res
        }
    }
    for (; index != res.length; index++) {
        res[index] = minKth; // 将剩余的位置填充为minKth
    }
    return res;
}

private int getMinKthByBFPRT(int[] arr, int k) {
    int[] copyArr = copyArray(arr); // 创建原数组的副本
    return select(copyArr, 0, copyArr.length - 1, k - 1); // 寻找第k小的数
}

private int[] copyArray(int[] arr) {
    int[] res = new int[arr.length];
    for (int i = 0; i != res.length; i++) {
        res[i] = arr[i]; // 将原数组的元素复制到副本数组中
    }
    return res;
}

private int select(int[] arr, int begin, int end, int i) {
    if (begin == end) {
        return arr[begin]; // 数组中只有一个元素,直接返回该元素
    }
    int pivot = medianOfMedians(arr, begin, end); // 获取pivot(基准)值
    int[] pivotRange = partition(arr, begin, end, pivot); // 将数组分为小于、等于和大于pivot的三个部分
    if (i >= pivotRange[0] && i <= pivotRange[1]) {
        return arr[i]; // 如果要找的位置i在等于部分的范围内,直接返回arr[i]
    } else if (i < pivotRange[0]) {
        return select(arr, begin, pivotRange[0] - 1, i); // 在左半部分递归查找
    } else {
        return select(arr, pivotRange[1] + 1, end, i); // 在右半部分递归查找
    }
}

private 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); // 将小于pivotValue的元素交换到左边
            cur++;
        } else if (arr[cur] > pivotValue) {
            swap(arr, cur, --big); // 将大于pivotValue的元素交换到右边
        } else {
            cur++;
        }
    }
    int[] range = new int[2];
    range[0] = small + 1; // 等于部分的起始位置
    range[1] = big - 1; // 等于部分的结束位置
    return range;
}

private 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 = begin + 4;
        mArr[i] = getMedian(arr, beginI, Math.min(end, endI)); // 获取子数组的中位数
    }
    return select(mArr, 0, mArr.length - 1, mArr.length / 2); // 递归调用select方法找到mArr的中位数
}

private 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]; // 返回中位数
}

private 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 a, int b) {
    int temp = arr[a];
    arr[a] = arr[b];
    arr[b] = temp;
}

主要的方法是`getMinKNumsByBFPRT`、`getMinKthByBFPRT`和`select`。

`getMinKNumsByBFPRT`方法:
- 首先判断k的值是否合法,如果不合法则直接返回原数组。
- 创建一个大小为k的结果数组res。
- 调用`getMinKthByBFPRT`方法获取数组中第k小的数minKth。
- 遍历原数组,将小于minKth的数放入结果数组res。
- 遍历结束后,如果结果数组res还没有填满,将剩余的位置填充为minKth。
- 返回结果数组res。

`getMinKthByBFPRT`方法:
- 调用`copyArray`方法创建一个原数组的副本copyArr。
- 调用`select`方法在copyArr中找到第k小的数,传入参数开始位置begin为0,结束位置end为copyArr的长度减1,要找的位置i为k-1。
- 返回找到的第k小的数。

`select`方法:
- 如果开始位置和结束位置相等,说明只有一个元素,直接返回这个元素。
- 调用`medianOfMedians`方法获取数组中的中位数作为pivot。
- 调用`partition`方法将数组分为小于、等于和大于pivot的三个部分,返回等于部分的范围[pivotRange[0], pivotRange[1]]。
- 如果要找的位置i在等于部分的范围内,说明找到了,直接返回arr[i]。
- 如果要找的位置i在等于部分的左边(pivotRange[0]左侧),递归调用select方法在左半部分查找。
- 如果要找的位置i在等于部分的右边(pivotRange[1]右侧),递归调用select方法在右半部分查找。

`partition`方法:
- 初始化small为开始位置-1,cur为开始位置,big为结束位置+1。
- 遍历数组,如果当前元素小于pivotValue,则将其与small+1位置的元素交换,并将small加1。
- 如果当前元素大于pivotValue,则将其与big-1位置的元素交换,并将big减1。
- 如果当前元素等于pivotValue,则继续遍历。
- 返回等于部分的范围[small+1, big-1]。

`medianOfMedians`方法:
- 计算数组长度num。
- 根据num计算mArr的长度,mArr中保存了原数组每个五个元素的中位数。
- 遍历原数组,每次取五个元素,调用`getMedian`方法获取其中位数,并保存在mArr中。
- 调用`select`方法在mArr中找到mArr的中位数作为pivot。
- 返回pivot。

`getMedian`方法:
- 调用`insertionSort`方法对数组进行插入排序。
- 计算中位数的位置mid。
- 返回数组中位置为mid的元素。

`insertionSort`方法:
- 对指定范围的数组进行插入排序。

这段代码使用了BFPRT算法,该算法可以在O(n)的时间复杂度内找到无序数组中第k小的数。然后根据这个数将数组分为小于和大于两部分,最后返回小于部分的k个数。这样就得到了最小的k个数。

四 需要排序的最短子数组长度

【题目】

给定一个无序数组arr,求出需要排序的最短子数组长度。例如:arr=[1,5,3,4,2,6,7]返回4,因为只有[5,3,4,2]需要排序。

public 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) { // 如果无序的位置索引为-1,说明数组已经有序,返回0
        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; // 返回无序子数组的长度
}

段代码使用了两次遍历的方法来解决问题。

  1. 首先,从数组的末尾开始向前遍历,找到最后一个破坏升序的元素,记录其索引为 noMinIndex,同时更新最小值 min
  2. 如果 noMinIndex 仍然为 -1,说明数组已经有序,返回0。
  3. 接下来,从数组的开头开始向后遍历,找到第一个破坏降序的元素,记录其索引为 noMaxIndex,同时更新最大值 max
  4. 返回无序子数组的长度,即 noMaxIndex - noMinIndex + 1

该算法的时间复杂度为 O(N),其中 N 是数组的长度。因为需要遍历数组两次,每次遍历的时间复杂度都是 O(N)。额外使用的空间复杂度为 O(1)。

五 在数组中找到出现次数大于N/K的数

【题目】给定一个整型数组arr,打印其中出现次数大于一半的数,如果没有这样的数,打印提示信息。进阶问题:给定一个整型数组arr,再给定一个整数K,打印所有出现次数大于N/K的数,如果没有这样的数,打印提示信息。

【要求】原问题要求时间复杂度为 O(N),额外空间复杂度为 O(1)。进阶问题要求时间复杂度为O(N×K),额外空间复杂度为O(K)。

原题解法

public 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.");
    }
}
  1. 遍历数组 arr,通过摩尔投票算法找到候选数:
    • 如果 times 为0,将当前元素设为候选数,并将 times 设置为1。
    • 如果当前元素与候选数相等,将 times 加1。
    • 如果当前元素与候选数不相等,将 times 减1。
  2. 再次遍历数组 arr,统计候选数的出现次数,如果出现次数大于一半,输出候选数;否则,打印"no such number."。

扩展问题解法

public 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<>(); // 候选数的哈希表,键为候选数,值为出现次数

    // 统计每个数的出现次数
    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) { // 当候选数的个数达到K-1时,将候选数的出现次数全部减1
                allCandsMinusOne(cands);
            } else {
                cands.put(arr[i], 1);
            }
        }
    }

    HashMap<Integer, Integer> reals = getReals(arr, cands); // 获取候选数的实际出现次数

    boolean hasPrint = false; // 是否已经输出了结果

    // 输出出现次数大于 N/K 的数
    for (Map.Entry<Integer, Integer> set : cands.entrySet()) {
        Integer key = set.getKey();
        if (reals.get(key) > arr.length / K) {
            hasPrint = true;
            System.out.print(key + " ");
        }
    }

    if (!hasPrint) {
        System.out.println("no such number.");
    }
}
//函数 allCandsMinusOne(HashMap<Integer, Integer> map) 的作用是将哈希表 map 中的所有候选数的出现次数减1,如果候选数的出现次数为1,则从哈希表中删除该候选数。
    private void allCandsMinusOne(HashMap<Integer, Integer> map) {
        List<Integer> removeList=new LinkedList<Integer>();
        for (Map.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);

        }

    }
//函数 getReals(int[] arr, HashMap<Integer, Integer> cands) 的作用是遍历数组 arr,统计每个候选数的实际出现次数,并返回一个哈希表,键为候选数,值为实际出现次数。
    private 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;
    }
  1. 判断K的值是否有效,如果K小于2,则打印"the value of K is invalid.",并返回。
  2. 创建一个哈希表 cands,用于存储候选数及其出现次数。
  3. 遍历数组 arr,对于每个元素:
    • 如果 cands 中包含该元素,将该元素的出现次数加1。
    • 如果 cands 中不包含该元素:
      • 如果 cands 的大小已经达到 K-1,调用 allCandsMinusOne(cands) 将候选数的出现次数全部减1,然后继续下一轮循环。
      • 如果 cands 的大小还未达到 K-1,将该元素加入 cands,出现次数设为1。
  4. 调用 getReals(arr, cands) 获取候选数的实际出现次数。
  5. 遍历 cands,对于每个候选数:
    • 如果实际出现次数大于 arr.length/K,输出该候选数。
  6. 如果没有输出结果,则打印"no such number."

函数 allCandsMinusOne(HashMap<Integer, Integer> map) 的作用是将哈希表 map 中的所有候选数的出现次数减1,如果候选数的出现次数为1,则从哈希表中删除该候选数。

函数 getReals(int[] arr, HashMap<Integer, Integer> cands) 的作用是遍历数组 arr,统计每个候选数的实际出现次数,并返回一个哈希表,键为候选数,值为实际出现次数。

六 在行列都排好序的矩阵中找指定数

【题目】给定一个N×M的整型矩阵matrix和一个整数K,matrix的每一行和每一列都是排好序的。实现一个函数,判断K是否在matrix中。例如:

如果K为7,返回true;如果K为6,返回false。

【要求】时间复杂度为O(N+M),额外空间复杂度为O(1)。

  public boolean isContains(int[][] matrix,int K)
    {
        int row=0;
        int col=matrix[0].length-1;
        while (row< matrix.length&&col>-1)
        {
            if (matrix[row][col]==K)
            {
                return true;
            }
            else if(matrix[row][col]>K)
            {
                col--;
            }
            else {
                row++;
            }
        }
        return false;
    }

1.从矩阵最右上角的数开始寻找(row=0,col=M-1)。

2.比较当前数matrix[row][col]与K的关系:

      ● 如果与K相等,说明已找到,直接返回true。

      ● 如果比K大,因为矩阵每一列都已排好序,所以在当前数所在的列中,处于当前数下方的数都会比K大,则没有必要继续在第col列上寻找,令col=col-1,重复步骤2。

      ● 如果比K小,因为矩阵每一行都已排好序,所以在当前数所在的行中,处于当前数左方的数都会比K小,则没有必要继续在第row行上寻找,令row=row+1,重复步骤2。

3.如果找到越界都没有发现与K相等的数,则返回false。

七  最长的可整合子数组的长度

【题目】

先给出可整合数组的定义:如果一个数组在排序之后,每相邻两个数差的绝对值都为 1,则该数组为可整合数组。

例如,[5,3,4,6,2]排序之后为[2,3,4,5,6],符合每相邻两个数差的绝对值都为1,所以这个数组为可整合数组。给定一个整型数组arr,请返回其中最大可整合子数组的长度。

例如,[5,5,3,2,6,4,3]的最大可整合子数组为[5,3,2,6,4],所以返回5。

public int getLIL2(int[] arr) {
    if (arr == null || arr.length == 0) {
        return 0;
    }
    int max=0;  // 当前子数组的最大值
    int min=0;  // 当前子数组的最小值
    int len=0;  // 最大可整合子数组的长度
    HashSet<Integer> set = new HashSet<Integer>();  // 用于存储判断是否可整合的数字
    for (int i = 0; i < arr.length ; i++) {  // 外层循环遍历数组,作为子数组的起始位置
        max = Integer.MIN_VALUE;  // 初始化当前子数组的最大值为最小整数
        min = Integer.MAX_VALUE;  // 初始化当前子数组的最小值为最大整数
        for (int j = i; j < arr.length; j++) {  // 内层循环遍历从起始位置开始的子数组
            if (set.contains(arr[j])) {  // 如果set中已经存在当前元素 arr[j],则子数组不可整合,跳出内层循环
                break;
            }
            set.add(arr[j]);  // 将当前元素 arr[j] 添加到set中
            max = Math.max(max, arr[j]);  // 更新当前子数组的最大值
            min = Math.min(min, arr[j]);  // 更新当前子数组的最小值
            if (max - min == j - i) {  // 判断当前子数组是否可整合,即最大值与最小值之差是否等于数组下标之差
                len = Math.max(len, j - i + 1);  // 更新最大可整合子数组的长度
            }
        }
        set.clear();  // 清空set,准备计算下一个起始位置的子数组
    }
    return len;  // 返回最大可整合子数组的长度
}

这个解法的思路是通过两层循环遍历数组,以不同的起始位置作为子数组的起点,然后在内层循环中判断以该起始位置为起点的子数组是否是可整合的。具体步骤如下:

  1. 如果给定的数组为null或长度为0,说明没有可整合的子数组,直接返回0。

  2. 初始化最大可整合子数组的长度为0。

  3. 创建一个HashSet,用于存储判断是否可整合的数字。

  4. 外层循环遍历数组,将不同的起始位置作为子数组的起点。

  5. 在内层循环中,从起始位置开始,依次向后遍历数组的元素。

  6. 如果在HashSet中已经存在当前元素,说明子数组不可整合,跳出内层循环。

  7. 如果HashSet中不存在当前元素,将当前元素添加到HashSet中,同时更新当前子数组的最大值和最小值。

  8. 判断当前子数组是否可整合,即最大值与最小值之差是否等于数组下标之差。

  9. 如果当前子数组可整合,更新最大可整合子数组的长度。

  10. 内层循环结束后,清空HashSet,准备计算下一个起始位置的子数组。

  11. 返回最大可整合子数组的长度。

这个算法的时间复杂度为O(N^2),其中N为给定数组的长度。因为两层循环遍历了数组。空间复杂度为O(N),用于存储HashSet。

八 不重复打印排序数组中相加和为给定值的所有二元组和三元组

【题目】给定排序数组arr和整数k,不重复打印arr中所有相加和为k的不降序二元组。

例如,arr=[-8,-4,-3,0,1,2,4,5,8,9],k=10,打印结果为

补充问题:给定排序数组arr和整数k,不重复打印arr中所有相加和为k的不降序三元组。

例如,arr=[-8,-4,-3,0,1,2,4,5,8,9],k=10,

打印结果为:

【难度】尉 ★★☆☆

/* 
 * 这段代码实现了打印数组中独特二元组和三元组的功能。
 * 
 * printUniquePair函数用于在数组中查找并打印满足条件arr[left] + arr[right] = k的二元组。
 * 参数arr是要查找的整数数组,参数k是目标和。
 * 
 * printUniqueTriad函数用于在数组中查找并打印满足条件arr[i] + arr[left] + arr[right] = k的三元组。
 * 参数arr是要查找的整数数组,参数k是目标和。
 * 
 * 注意:这段代码中已经假设传入的数组arr已经排序。
 */

public void printUniquePair(int[] arr, int k) {
    if (arr == null || arr.length < 2) {
        return; // 数组为空或长度小于2,直接返回
    }
    
    int left = 0; // 左指针
    int right = arr.length - 1; // 右指针
    
    while (left < right) {
        if (arr[left] + arr[right] < k) {
            left++; // 当前元素对的和小于k,左指针右移
        } else if (arr[left] + arr[right] > k) {
            right--; // 当前元素对的和大于k,右指针左移
        } else {
            // 当前元素对的和等于k
            if (left == 0 || arr[left - 1] != arr[left]) {
                System.out.println(arr[left] + "," + arr[right]); // 打印满足条件的二元组
            }
            left++; // 左指针右移
            right--; // 右指针左移
        }
    }
}
//● 当三元组的第一个值为-8时,寻找-8后面的子数组中所有相加为18的不重复二元组。
//● 当三元组的第一个值为-4时,寻找-4后面的子数组中所有相加为14的不重复二元组。
//● 当三元组的第一个值为-3时,寻找-3后面的子数组中所有相加为13的不重复二元组

public void printUniqueTriad(int[] arr, int k) {
    if (arr == null || arr.length < 3) {
        return; // 数组为空或长度小于3,直接返回
    }
    
    for (int i = 0; i < arr.length - 2; i++) {
        if (i == 0 || arr[i] != arr[i - 1]) {
            printReset(arr, i, i + 1, arr.length - 1, k - arr[i]); // 进行三元组的查找和打印
        }
    }
}

private void printReset(int[] arr, int f, int l, int r, int k) {
    while (l < r) {
        if (arr[l] + arr[r] < k) {
            l++; // 当前三元组的和小于k,左指针右移
        } else if (arr[l] + arr[r] > k) {
            r--; // 当前三元组的和大于k,右指针左移
        } else {
            // 当前三元组的和等于k
            if (l == f + 1 || arr[l - 1] != arr[l]) {
                System.out.println(arr[f] + "," + arr[l] + "," + arr[r]); // 打印满足条件的三元组
            }
            l++; // 左指针右移
            r--; // 右指针左移
        }
    }
}

九  未排序正数数组中累加和为给定值的最长子数组长度

【题目】给定一个数组arr,该数组无序,但每个值均为正数,再给定一个正数k。求arr的所有子数组中所有元素相加和为k的最长子数组长度。例如,arr=[1,2,1,1,1],k=3。累加和为3的最长子数组为[1,1,1],所以结果返回3。

/**
 * 获取arr数组中,所有元素相加和为k的最长子数组的长度
 * @param arr 给定的数组
 * @param k 目标累加和
 * @return 最长子数组的长度
 */
public int getMaxLength(int[] arr, int k) {
    // 如果数组为空、长度为0或k小于等于0,直接返回0
    if (arr == null || arr.length == 0 || k <= 0) {
        return 0;
    }
    
    int left = 0; // 左指针,用于指向当前子数组的起始位置
    int right = 0; // 右指针,用于指向当前子数组的结束位置
    int sum = arr[0]; // 当前子数组的累加和,初始化为第一个元素的值
    int len = 0; // 最长子数组的长度,初始化为0
    
    // 使用滑动窗口技巧遍历数组
    while (right < arr.length) {
        if (sum == k) { // 当前子数组的累加和等于目标累加和k
            len = Math.max(len, right - left + 1); // 更新最长子数组的长度为当前子数组的长度与之前最长长度的较大值
            sum -= arr[left++]; // 左指针向右移动一位,减去移出窗口的元素
        } else if (sum < k) { // 当前子数组的累加和小于目标累加和k
            right++; // 右指针向右移动一位
            if (right == arr.length) { // 如果右指针移到了数组的末尾,跳出循环
                break;
            }
            sum += arr[right]; // 将移进窗口的元素添加到累加和中
        } else { // 当前子数组的累加和大于目标累加和k
            sum -= arr[left++]; // 左指针向右移动一位,减去移出窗口的元素
        }
    }

    return len; // 返回最长子数组的长度
}

十 未排序数组中累加和为给定值的最长子数组系列问题

【题目】给定一个无序数组arr,其中元素可正、可负、可0。给定一个整数k,求arr所有的子数组中累加和为k的最长子数组长度。

补充问题 1:给定一个无序数组 arr,其中元素可正、可负、可 0。求 arr所有的子数组中正数与负数个数相等的最长子数组长度。

补充问题2:给定一个无序数组arr,其中元素只是1或0。求arr所有的子数组中0和1个数相等的最长子数组长度。

具体步骤如下:

  1. 创建一个哈希表 map,用于存储累加和以及对应的位置。
  2. 初始化累加和为 0,并将累加和 0 和位置 -1 放入哈希表 map。
  3. 遍历数组 arr,遍历过程中累加当前位置的元素,记为 sum。对于每个位置 i,计算累加和 sum-k,在哈希表 map 中查找是否存在该累加和,如果存在,则更新最长子数组长度。
  4. 如果哈希表中已存在累加和 sum,则不更新;如果不存在,则将累加和 sum 与位置 i 放入哈希表 map。
  5. 最终得到累加和为 k 的最长子数组长度。

/**
 * 给定一个数组arr和一个整数k,求arr所有的子数组中累加和为k的最长子数组长度
 * @param arr 给定的数组
 * @param k 目标累加和
 * @return 累加和为k的最长子数组长度
 */
public int maxLength(int[] arr, int k) {
    if (arr == null || arr.length == 0) {
        return 0;
    }

    // 使用 HashMap 存储累加和以及对应的位置
    HashMap<Integer, Integer> map = new HashMap<>();
    map.put(0, -1); // 初始化,累加和为 0 时对应的位置为 -1
    int maxLen = 0; // 存储最长子数组长度
    int sum = 0; // 记录累加和

    // 遍历数组,查找累加和为 k 的最长子数组长度
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i]; // 计算当前位置的累加和
        if (map.containsKey(sum - k)) { // 判断是否存在累加和为 sum-k 的子数组
            maxLen = Math.max(i - map.get(sum - k), maxLen); // 更新最长子数组长度
        }
        if (!map.containsKey(sum)) {
            map.put(sum, i); // 如果当前累加和不存在于HashMap中,则将其放入HashMap
        }
    }

    return maxLen; // 返回累加和为k的最长子数组长度
}

第一个补充问题是先把数组arr 中的正数全部变成1,负数全部变成-1,0不变,然后求累加和为0的最长子数组长度即可。第二个补充问题是先把数组arr中的0全部变成-1,1不变,然后求累加和为0的最长子数组长度即可。两个补充问题的代码略。

十一   未排序数组中累加和小于或等于给定值的最长子数组长度

【题目】给定一个无序数组arr,其中元素可正、可负、可0。给定一个整数k,求arr所有的子数组中累加和小于或等于k的最长子数组长度。例如:arr=[3,-2,-4,0,6],k=-2,相加和小于或等于-2的最长子数组为{3,-2,-4,0},所以结果返回4。

【要求】实现出时间复杂度为O(N)的方法。

要实现时间复杂度为O(N)的方法,可以使用滑动窗口技巧来解决这个问题。通过维护一个窗口,使其以O(1)的时间复杂度内可以滑动,从而实现O(N)的时间复杂度。

/**
 * 时间复杂度为O(NlogN),额外空间复杂度为O(N)的解法
 * @param arr 给定的数组
 * @param k 目标累加和
 * @return 小于或等于k的最长子数组长度
 */
public int maxLength(int[] arr, int k) {
    int[] h = new int[arr.length + 1]; // 初始化一个累加和数组h
    int sum = 0;
    h[0] = sum;
    
    // 计算累加和数组h
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i];
        h[i + 1] = Math.max(sum, h[i]);
    }
    
    sum = 0;
    int res = 0;
    int pre = 0;
    int len = 0;
    
    // 遍历数组,求小于或等于k的最长子数组长度
    for (int i = 0; i < arr.length; i++) {
        sum += arr[i];
        pre = getLessIndex(h, sum - k); // 获取小于sum-k的最大索引值
        len = pre == -1 ? 0 : i - pre + 1; // 计算当前子数组的长度
        res = Math.max(res, len); // 更新最大子数组长度
    }

    return res;
}

// 获取小于num的最大索引值
private int getLessIndex(int[] arr, int num) {
    int low = 0;
    int high = arr.length - 1;
    int mid = 0;
    int res = -1;
    
    // 二分查找
    while (low <= high) {
        mid = (low + high) / 2;
        if (arr[mid] >= num) {
            res = mid;
            high = mid - 1;
        } else {
            low = mid + 1;
        }
    }

    return res;
}

/**
 * 时间复杂度为O(N),额外空间复杂度为O(N)的最优解
 * @param arr 给定的数组
 * @param k 目标累加和
 * @return 小于或等于k的最长子数组长度
 */
public int maxLengthAwesome(int[] arr, int k) {
    if (arr == null || arr.length == 0) {
        return 0;
    }
    
    int[] minSums = new int[arr.length]; // 存储以每个位置开头的累加和小于或等于给定值的最长子数组累加和
    int[] minSumEnds = new int[arr.length]; // 存储以每个位置开头的累加和小于或等于给定值的最长子数组的结束位置索引
    
    // 从后往前计算minSums和minSumEnds数组
    for (int i = arr.length - 2; i >= 0; i--) {
        if (minSums[i + 1] < 0) {
            minSums[i] = arr[i] + minSums[i + 1];
            minSumEnds[i] = minSumEnds[i + 1];
        } else {
            minSums[i] = arr[i];
            minSumEnds[i] = i;
        }
    }

    int res = 0; // 存储最终结果
    int end = 0;
    int sum = 0;
    
    // 遍历数组,找出小于或等于k的最长子数组长度
    for (int i = 0; i < arr.length; i++) {
        // 在以i为起始位置的情况下,寻找累加和小于或等于k的子数组
        while (end < arr.length && sum + minSums[end] <= k) {
            sum += minSums[end];
            end = minSumEnds[end] + 1;
        }
        res = Math.max(res, end - i); // 更新结果
        if (end > i) { // 窗口内还有数
            sum -= arr[i];
        } else { // 窗口内已经没有数了,从i开始的子数组累加和都不可能小于或等于k
            end = i + 1;
        }
    }

    return res;
}

十二   计算数组的小和

【题目】

数组小和的定义如下:例如,数组s=[1,3,5,2,4,6],在s[0]的左边小于或等于s[0]的数的和为0;在s[1]的左边小于或等于s[1]的数的和为1;在s[2]的左边小于或等于s[2]的数的和为1+3=4;在s[3]的左边小于或等于s[3]的数的和为1;在s[4]的左边小于或等于s[4]的数的和为1+3+2=6;在s[5]的左边小于或等于s[5]的数的和为1+3+5+2+4=15。所以s的小和为0+1+4+1+6+15=27。

给定一个数组s,实现函数返回s的小和。

public class ArraySmallSum {

    // 计算数组的小和
    public int getSmallSum(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;
        }
        return func(arr, 0, arr.length - 1);  // 调用递归函数进行求解
    }

    // 递归函数,使用归并排序的方式求解数组的小和
    private int func(int[] s, int l, int r) {
        // 如果左指针等于右指针,表示数组只有一个元素,小和为0
        if (l == r) {
            return 0;
        }
        int mid = (l + r) / 2;  // 计算中间位置
        // 分别对左右两个子数组进行求解,并返回两部分的小和之和,以及合并后的小和
        return func(s, l, mid) + func(s, mid + 1, r) + merge(s, l, mid, r);
    }

    // 合并左右两个有序子数组,并计算小和
    private int merge(int[] s, int left, int mid, int right) {
        int[] h = new int[right - left + 1];  // 辅助数组
        int hi = 0;  // 辅助数组的指针
        int i = left;  // 左侧子数组的起始位置
        int j = mid + 1;  // 右侧子数组的起始位置
        int smallSum = 0;  // 小和
        // 合并左右两个有序子数组,并同时计算小和
        while (i <= mid && j <= right) {
            if (s[i] <= s[j]) {
                // 右侧子数组的元素大于等于左侧子数组的元素,不符合小和定义
                // 将左侧子数组的元素放入辅助数组中,并累加小和
                smallSum += s[i] * (right - j + 1);
                h[hi++] = s[i++];
            } else {
                // 将右侧子数组的元素放入辅助数组中
                h[hi++] = s[j++];
            }
        }
        // 将左侧子数组剩余的元素放入辅助数组中
        while (i <= mid) {
            h[hi++] = s[i++];
        }
        // 将右侧子数组剩余的元素放入辅助数组中
        while (j <= right) {
            h[hi++] = s[j++];
        }
        // 将辅助数组的元素复制回原数组
        for (int k = 0; k != h.length; k++) {
            s[left++] = h[k];
        }
        return smallSum;  // 返回合并后的小和
    }

    public static void main(String[] args) {
        ArraySmallSum arraySmallSum = new ArraySmallSum();
        int[] array = {1, 3, 5, 2, 4, 6};
        int smallSum = arraySmallSum.getSmallSum(array);
        System.out.println("小和为:" + smallSum);
    }
}

此算法使用了一个辅助数组来临时存储合并过程中的结果,并在合并的同时计算了小和。整体的时间复杂度为O(nlogn),其中n是数组的长度。

十三  自然数数组的排序

【题目】

给定一个长度为N的整型数组arr,其中有N个互不相等的自然数1~N。请实现arr的排序,但是不要把下标0~N-1位置上的数通过直接赋值的方式替换成1~N。

【要求】

时间复杂度为O(N),额外空间复杂度为O(1)。

public class NaturalNumberSort {

    /**
     * 方法1:使用临时变量交换元素位置进行排序
     *
     * @param arr 给定的整型数组
     */
    public void sort1(int[] arr) {
        int tmp = 0;
        int next = 0;
        for (int i = 0; i != arr.length; i++) {
            tmp = arr[i];
            // 如果当前位置i的元素不等于i+1,即不在正确的位置上
            while (arr[i] != i + 1) {
                next = arr[tmp - 1];  // 获取tmp值应该放置的位置上的元素
                arr[tmp - 1] = tmp;  // 将tmp值放在对应位置上
                tmp = next;  // 更新tmp值为next,即继续进行后续交换操作
            }
        }
    }

    /**
     * 方法2:直接交换元素位置进行排序
     *
     * @param arr 给定的整型数组
     */
    public void sort2(int[] arr) {
        for (int i = 0; i != arr.length; i++) {
            int tmp = 0;
            // 如果当前位置i的元素不等于i+1,即不在正确的位置上
            while (arr[i] != i + 1) {
                tmp = arr[arr[i] - 1];  // 获取arr[i]值应该放置的位置上的元素
                arr[arr[i] - 1] = arr[i];  // 将arr[i]值放在对应位置上
                arr[i] = tmp;  // 更新arr[i]值为tmp,即继续进行后续交换操作
            }
        }
    }

    public static void main(String[] args) {
        NaturalNumberSort sorter = new NaturalNumberSort();
        int[] arr = {3, 2, 4, 1, 5};
        sorter.sort1(arr);
        for (int num : arr) {
            System.out.print(num + " ");
        }
        System.out.println();

        int[] arr2 = {3, 2, 4, 1, 5};
        sorter.sort2(arr2);
        for (int num : arr2) {
            System.out.print(num + " ");
        }
    }
}

方法1使用临时变量的方式进行排序,主要思路是利用临时变量tmp保存当前位置的元素值,并不断通过索引tmp-1获取下一个位置的元素值,直到元素值arr[i]等于i+1为止,表示该位置已经在正确的位置上。

方法2直接交换元素位置进行排序,主要思路是直接通过索引进行元素交换,将元素值arr[i]放置到索引arr[i]-1上,而原先索引arr[i]-1上的元素值则放置在arr[i]上,不断进行交换操作,直到元素值arr[i]等于i+1为止。

两种方法的核心思想都是通过不断交换元素位置的方式将数组排序,时间复杂度为O(n),并且不需要额外的空间,满足题目的要求。

十四  奇数下标都是奇数或者偶数下标都是偶数

【题目】

给定一个长度不小于2的数组arr,实现一个函数调整arr,要么让所有的偶数下标都是偶数,要么让所有的奇数下标都是奇数。

【要求】

如果arr的长度为N,函数要求时间复杂度为O(N)、额外空间复杂度为O(1)。

public class EvenOddIndexSort {

    /**
     * 调整数组使得偶数下标都是偶数或奇数下标都是奇数
     *
     * @param arr 给定的整型数组
     */
    public void modify(int[] arr) {
        if (arr == null || arr.length < 2) {
            return;  // 如果数组长度小于2或为null,则直接返回,不需要调整
        }
        int even = 0;  // 偶数下标
        int odd = 1;   // 奇数下标
        int end = arr.length - 1;  // 数组的最后一个索引
        // 使用双指针,从数组两端向中间遍历
        while (even <= end && odd <= end) {
            if ((arr[end] & 1) == 0) {
                // 如果数组末尾元素为偶数,则将其与偶数下标的元素交换
                swap(arr, end, even);
                even += 2;  // 偶数下标后移两位
            } else {
                // 如果数组末尾元素为奇数,则将其与奇数下标的元素交换
                swap(arr, end, odd);
                odd += 2;   // 奇数下标后移两位
            }
        }
    }

    /**
     * 交换数组中指定索引位置的元素
     *
     * @param arr    给定的整型数组
     * @param index1 要交换的元素索引1
     * @param index2 要交换的元素索引2
     */
    private void swap(int[] arr, int index1, int index2) {
        int tmp = arr[index1];  // 临时保存index1位置的元素值
        arr[index1] = arr[index2];  // 将index2位置的元素值赋给index1
        arr[index2] = tmp;  // 将tmp值赋给index2,实现交换操作
    }

    public static void main(String[] args) {
        EvenOddIndexSort sorter = new EvenOddIndexSort();
        int[] arr = {3, 6, 7, 4, 5, 2, 8, 1};
        sorter.modify(arr);
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

这段代码中,首先通过检查数组长度是否小于2或者为null,进行了特殊情况的处理。然后使用双指针(即even和odd)从数组两端向中间遍历,根据数组末尾元素的奇偶性来决定与偶数下标或奇数下标的元素进行交换。

通过这种双指针的方式,可以在O(n)的时间复杂度内完成数组的调整,同时不需要使用额外的空间,符合题目的要求。

十五  子数组的最大累加和问题

【题目】

给定一个数组arr,返回子数组的最大累加和。例如,arr=[1,-2,3,5,-2,6,-1],所有的子数组中,[3,5,-2,6]可以累加出最大的和12,所以返回12。

【要求】

如果arr长度为N,要求时间复杂度为O(N)、额外空间复杂度为O(1)。

public class MaxSumOfSubarray {

    /**
     * 返回子数组的最大累加和
     *
     * @param arr 给定的整型数组
     * @return 子数组的最大累加和
     */
    public int maxSum(int[] arr) {
        if (arr == null || arr.length == 0) {
            return 0;  // 如果数组为空或长度为0,则返回0
        }
        int max = Integer.MIN_VALUE;  // 初始化最大累加和为最小整数值
        int cur = 0;  // 当前子数组的累加和

        for (int i = 0; i < arr.length; i++) {
            cur += arr[i];  // 将当前元素累加到cur中
            max = Math.max(max, cur);  // 比较当前累加和cur和最大累加和max,取较大的值作为新的最大累加和
            cur = cur < 0 ? 0 : cur;  // 如果当前子数组的累加和cur小于0,则将cur置为0,从下一个元素重新开始累加
        }
        return max;  // 返回最大累加和
    }

    public static void main(String[] args) {
        MaxSumOfSubarray calculator = new MaxSumOfSubarray();
        int[] arr = {1, -2, 3, 5, -2, 6, -1};
        int maxSum = calculator.maxSum(arr);
        System.out.println("子数组的最大累加和为:" + maxSum);
    }
}

这段代码使用了一种遍历数组的方法,单次遍历过程中实时更新当前子数组的累加和cur以及最大累加和max。

  1. 初始化最大累加和max为最小整数值,作为初始值。
  2. 遍历数组,对于每个元素:
    • 将当前元素累加到cur中,即cur += arr[i]。
    • 比较当前累加和cur和最大累加和max,取较大的值作为新的最大累加和,即max = Math.max(max, cur)。
    • 如果当前子数组的累加和cur小于0,说明累加到当前元素时的累加和产生了负效果,因此将cur置为0,从下一个元素重新开始累加。
  3. 遍历结束后,max即为子数组的最大累加和。

十六  子矩阵的最大累加和问题

【题目】给定一个矩阵matrix,其中的值有正、有负、有0,返回子矩阵的最大累加和。例如,矩阵matrix为:

其中,最大累加和的子矩阵为:

所以返回累加和209。例如,matrix为:

其中,最大累加和的子矩阵为:

所以返回累加和4。

public int maxSum(int[][] m)
{
    // 判断输入矩阵是否为空
    if (m == null || m.length == 0 || m[0].length == 0) {
        return 0;
    }
    
    int max = Integer.MIN_VALUE; // 用于记录子矩阵的最大累加和
    int cur = 0; // 用于记录当前累加和
    int[] s = null; // 用于记录每列的累加和
    
    // 遍历矩阵的每一行作为子矩阵的起始行
    for (int i = 0; i != m.length; i++) {
        s = new int[m[0].length]; // 初始化累加和数组
        // 从起始行开始遍历到最后一行,作为子矩阵的结束行
        for (int j = i; j != m.length; j++) {
            cur = 0; // 当前累加和清零
            // 对每列进行累加
            for (int k = 0; k != s.length; k++) {
                s[k] += m[j][k]; // 更新累加和数组
                cur += s[k]; // 更新当前累加和
                max = Math.max(max, cur); // 更新最大累加和
                cur = cur < 0 ? 0 : cur; // 如果当前累加和为负数,则重新开始累加
            }
        }
    }
    return max; // 返回最大累加和
}

这段代码实现了一个时间复杂度为 O(n^3) 的算法来求解给定矩阵的子矩阵最大累加和。具体来说,它利用了动态规划的思想,通过遍历矩阵中的每个元素作为子矩阵的右下角,不断更新累加和并找出最大值的过程来求解最大累加和。因此,这种解法是有效的。

十七  在数组中找到一个局部最小的位置

【题目】

定义局部最小的概念。arr长度为 1时,arr[0]是局部最小。arr的长度为N(N>1)时,如果 arr[0]<arr[1],那么 arr[0]是局部最小;如果 arr[N-1]<arr[N-2],那么 arr[N-1]是局部最小;如果0<i<N-1,既有arr[i]<arr[i-1],又有arr[i]<arr[i+1],那么arr[i]是局部最小。给定无序数组arr,已知arr中任意两个相邻的数都不相等。写一个函数,只需返回arr中任意一个局部最小出现的位置即可。

public int getLessIndex(int[] arr)
{
    // 判断输入数组是否为空
    if (arr == null || arr.length == 0) {
        return -1; // 若为空,则返回-1表示无局部最小
    }
    
    // 如果数组长度为1或者arr[0]小于arr[1],则arr[0]为局部最小
    if (arr.length == 1 || arr[0] < arr[1]) {
        return 0;
    }
    
    // 如果arr[arr.length-1]小于arr[arr.length-2],则arr[arr.length-1]为局部最小
    if (arr[arr.length - 1] < arr[arr.length - 2]) {
        return arr.length - 1;
    }
    
    int left = 1; // 左边界
    int right = arr.length - 2; // 右边界
    int mid = 0; // 中间位置
    
    // 使用二分查找找到局部最小出现的位置
    while (left < right) {
        mid = (left + right) / 2; // 计算中间位置
        
        // 如果arr[mid]比它的左侧元素更大,说明局部最小在mid的左侧
        if (arr[mid] > arr[mid - 1]) {
            right = mid - 1; // 更新右边界
        }
        // 如果arr[mid]比它的右侧元素更大,说明局部最小在mid的右侧
        else if (arr[mid] > arr[mid + 1]) {
            left = mid + 1; // 更新左边界
        }
        // 否则,arr[mid]即为局部最小
        else {
            return mid;
        }
    }
    return left; // 返回局部最小的位置
}

这种解法采用了二分查找的思想来寻找数组中的局部最小值的位置。下面我会逐步详细解释这种解法的思路:

1. 首先,我们判断输入数组是否为空,若为空则返回-1表示无局部最小。

2. 接着,我们对数组的长度进行特殊情况处理。如果数组长度为1,或者数组的第一个元素arr[0]比第二个元素arr[1]小,那么arr[0]就是一个局部最小,因此我们可以直接返回0。

3. 同理,如果数组的最后一个元素arr[arr.length-1]比倒数第二个元素arr[arr.length-2]小,那么arr[arr.length-1]就是一个局部最小,我们可以返回arr.length-1。

4. 如果经过上述特殊情况的判断,仍未在数组的首尾找到局部最小,那么我们就需要使用二分查找来寻找局部最小值的位置了。

5. 初始化左边界为1(因为我们已经检查过arr[0])和右边界为arr.length-2(因为我们已经检查过arr[arr.length-1]),然后定义一个中间位置mid。

6. 在每一步循环中,计算mid=(left+right)/2,找到数组的中间位置。

7. 接下来,我们需要根据mid的值来确定局部最小值的位置。如果arr[mid]比它的左侧元素arr[mid-1]大,则说明局部最小值在mid的左侧,因此我们可以将右边界更新为mid-1。

8. 如果arr[mid]比它的右侧元素arr[mid+1]大,则说明局部最小值在mid的右侧,因此我们可以将左边界更新为mid+1。

9. 如果既不满足7也不满足8,那么arr[mid]就是一个局部最小值,我们可以直接返回mid作为答案。

10. 重复执行步骤6到9,直到最终找到局部最小的位置。

11. 如果最终left和right相等,那么这个位置就是局部最小的位置,我们可以返回left作为答案。

总体上来说,这种解法利用二分查找的方式逐步缩小搜索范围,最终能够在较快的时间内找到一个局部最小值的位置。由于每次都将搜索范围缩小一半,因此时间复杂度为O(logn)。希望上面的解释能够帮助你理解这种解法。

十八   数组中子数组的最大累乘积

【题目】

给定一个double类型的数组arr,其中的元素可正、可负、可0,返回子数组累乘的最大乘积。例如,arr=[-2.5,4,0,3,0.5,8,-1],子数组[3,0.5,8]累乘可以获得最大的乘积12,所以返回12。

public double maxProduct(double[] arr)
{
    // 判断输入数组是否为空
    if (arr == null || arr.length == 0) {
        return 0; // 若为空,则返回0
    }
    
    double max = arr[0]; // 初始化最大乘积为arr[0]
    double min = arr[0]; // 初始化最小乘积为arr[0]
    double res = arr[0]; // 初始化结果为arr[0]
    double maxEnd = 0; // 用于存储以当前元素为结尾的最大乘积
    double minEnd = 0; // 用于存储以当前元素为结尾的最小乘积
    
    // 遍历数组,更新最大乘积和最小乘积
    for (int i = 1; i < arr.length; ++i) {
        maxEnd = max * arr[i]; // 以当前元素为结尾的最大乘积
        minEnd = min * arr[i]; // 以当前元素为结尾的最小乘积
        max = Math.max(Math.max(maxEnd, minEnd), arr[i]); // 更新最大乘积
        min = Math.min(Math.min(maxEnd, minEnd), arr[i]); // 更新最小乘积
        res = Math.max(res, max); // 更新结果
    }
    
    return res; // 返回最大乘积
}

这种解法使用了动态规划的思想来找到数组中所有连续子数组的最大乘积。下面我会逐步详细解释这种解法的思路:

1. 首先,判断输入数组是否为空或者长度为0,如果是,直接返回0作为结果。

2. 初始化变量max、min和res为数组第一个元素arr[0],它们分别表示当前的最大乘积、最小乘积和最终的结果。

3. 初始化变量maxEnd和minEnd为0,它们分别用于存储以当前元素为结尾的最大乘积和最小乘积。

4. 接着,从数组的第二个元素开始,遍历数组。

5. 在每一步循环中,根据动态规划的思想,我们可以得到以当前元素为结尾的最大乘积和最小乘积。最大乘积maxEnd可以由前一个位置的最大乘积max乘以当前元素arr[i]得到,最小乘积minEnd可以由前一个位置的最小乘积min乘以当前元素arr[i]得到。

6. 更新最大乘积和最小乘积,将max设置为maxEnd、minEnd和当前元素arr[i]的最大值,将min设置为maxEnd、minEnd和当前元素arr[i]的最小值。

7. 不断更新结果res,将res设置为当前res和max的最大值,这样可以保证res为最大乘积。

8. 遍历完整个数组后,返回结果res作为最大乘积的结果。

总体上来说,这种解法使用动态规划的思想,通过不断更新最大乘积和最小乘积,得到最终的结果。由于只需遍历一遍数组,时间复杂度为O(n),其中n是数组的长度。希望上面的解释能够帮助你理解这种解法。

十九  打印N个数组整体最大的Top K

【题目】

有N个长度不一的数组,所有的数组都是有序的,请从大到小打印这N个数组整体最大的前K个数。例如,输入含有N行元素的二维数组可以代表N个一维数组。219,405,538,845,971 148,558 52,99,348,691再输入整数k=5,则打印:Top 5:971,845,691,558,538

public class HeapNode {
    public int value; // 值
    public int arrNum; // 哪个数组
    public int index; // 来自数组的哪个位置

    public HeapNode(int value, int arrNum, int index) {
        this.value = value;
        this.arrNum = arrNum;
        this.index = index;
    }
}

public void printTopK(int[][] matrix, int topK) {
    int heapSize = matrix.length;
    HeapNode[] heap = new HeapNode[heapSize];

    // 初始化堆
    for (int i = 0; i < heapSize; i++) {
        int index = matrix[i].length - 1;
        heap[i] = new HeapNode(matrix[i][index], i, index);
        heapInsert(heap, i);
    }

    System.out.println("TOP " + topK + ":");

    // 依次打印前K个最大的数
    for (int i = 0; i < topK && heapSize > 0; i++) {
        System.out.print(heap[0].value + " ");

        // 如果当前数组还有元素未被加入堆中,则将其加入堆
        if (heap[0].index > 0) {
            heap[0].value = matrix[heap[0].arrNum][--heap[0].index];
        } 
        // 如果当前数组的所有元素已经被加入堆中,则将堆顶元素与堆的最后一个元素交换,并调整堆结构
        else {
            swap(heap, 0, --heapSize);
            heapify(heap, 0, heapSize);
        }
    }
}

// 通过向上调整的方式,保持堆的结构特性
private void heapInsert(HeapNode[] heap, int index) {
    while (index != 0) {
        int parent = (index - 1) / 2;

        // 如果新插入的元素大于父节点的值,则交换它们的位置
        if (heap[parent].value < heap[index].value) {
            swap(heap, parent, index);
            index = parent;
        } 
        // 如果新插入的元素小于等于父节点的值,则停止调整
        else {
            break;
        }
    }
}

// 通过向下调整的方式,保持堆的结构特性
private void heapify(HeapNode[] heap, int index, int heapSize) {
    int left = index * 2 + 1;
    int right = index * 2 + 2;
    int largest = index;

    while (left < heapSize) {
        // 找到左右子节点中值最大的节点
        if (heap[left].value > heap[largest].value) {
            largest = left;
        }
        if (right < heapSize && heap[right].value > heap[largest].value) {
            largest = right;
        }

        // 如果最大值不是当前节点,则交换它们的位置
        if (largest != index) {
            swap(heap, largest, index);
            index = largest;
            left = index * 2 + 1;
            right = index * 2 + 2;
        } 
        // 如果最大值已经是当前节点,则停止调整
        else {
            break;
        }
    }
}

// 交换数组中两个元素的位置
private void swap(HeapNode[] heap, int index1, int index2) {
    HeapNode tmp = heap[index1];
    heap[index1] = heap[index2];
    heap[index2] = tmp;
}

这段代码实现了从N个有序数组中打印整体最大的前K个数。它使用了一个小根堆(数组heap)来存储当前的最大的K个数。首先,遍历所有的数组,用数组的最后一个元素初始化小根堆。然后,通过heapInsert方法将新元素插入到堆中,并保持堆的结构。接着,在打印前K个数时,不断从堆顶取出最大的元素并输出,然后将对应数组的前一个元素加入到堆中。当堆的大小为0或者已经达到K个数时停止。最后,使用heapify方法维持堆的性质。

这个解法的时间复杂度取决于堆的大小K。构建堆的时间复杂度为O(NlogM),其中N是数组的个数,M是最大的数组长度。打印前K个数的时间复杂度为O(KlogK)。因此,总体的时间复杂度为O((N+K)logM)。希望上面的解释能够帮助你理解这种解法。

二十 边界都是1的最大正方形大小

【题目】

给定一个N×N的矩阵matrix,在这个矩阵中,只有0和1两种值,返回边框全是1的最大正方形的边长长度。例如:

其中,边框全是1的最大正方形的大小为4×4,所以返回4。

public int getMaxSize(int[][] m) {
    // 创建用于存储右边界和下边界的数组
    int[][] right = new int[m.length][m[0].length];
    int[][] down = new int[m.length][m[0].length];
    // 计算右边界和下边界的数组
    setBorderMap(m, right, down);
    
    // 从最大边长开始遍历,判断是否存在边框大小为size的正方形
    for (int size = Math.min(m.length, m[0].length); size != 0; size--) {
        if (hasSizeOfBorder(size, right, down)) {
            return size;
        }
    }

    return 0;
}

/**
 * 判断是否存在边框大小为size的正方形
 * @param size 边框大小
 * @param right 右边界数组
 * @param down 下边界数组
 * @return 是否存在边框大小为size的正方形
 */
private boolean hasSizeOfBorder(int size, int[][] right, int[][] down) {
    // 遍历每个可能的左上角起点
    for (int i = 0; i != right.length - size + 1; i++) {
        for (int j = 0; j != right[0].length - size + 1; j++) {
            // 判断右边界、下边界、右下角位置的值是否都大于等于size,若是,则存在边框大小为size的正方形
            if (right[i][j] >= size && down[i][j] >= size
                    && right[i + size - 1][j] >= size
                    && down[i][j + size - 1] >= size) {
                return true;
            }
        }
    }
    return false;
}

/**
 * 计算右边界和下边界的数组
 * @param m 原始矩阵
 * @param right 右边界数组
 * @param down 下边界数组
 */
public void setBorderMap(int[][] m, int[][] right, int[][] down) {
    int r = m.length;
    int c = m[0].length;
    
    // 初始化右边界和下边界的最后一个元素
    if (m[r - 1][c - 1] == 1) {
        right[r - 1][c - 1] = 1;
        down[r - 1][c - 1] = 1;
    }
    
    // 计算最后一列的右边界和下边界
    for (int i = r - 2; i != -1; i--) {
        if (m[i][c - 1] == 1) {
            right[i][c - 1] = 1;
            down[i][c - 1] = down[i + 1][c - 1] + 1;
        }
    }
    
    // 计算最后一行的右边界和下边界
    for (int i = c - 2; i != -1; i--) {
        if (m[r - 1][i] == 1) {
            right[r - 1][i] = right[r - 1][i + 1] + 1;
            down[r - 1][i] = 1;
        }
    }
    
    // 计算其他位置的右边界和下边界
    for (int i = r - 2; i != -1; i--) {
        for (int j = c - 2; j != -1; j--) {
            if (m[i][j] == 1) {
                right[i][j] = right[i][j + 1] + 1;
                down[i][j] = down[i + 1][j] + 1;
            }
        }
    }
}

这段代码使用了动态规划的思路,通过计算右边界数组right和下边界数组down,然后从最大边长开始遍历,判断是否存在边框大小为size的正方形。

首先,在setBorderMap函数中,该函数用于计算右边界和下边界的数组。从右下角开始,初始位置的右边界和下边界值分别为1(如果该位置的值为1)。然后,从右下角往上和往左遍历,根据当前位置的值更新右边界和下边界的值,具体规则是如果当前位置的值为1,则该位置的右边界值为相邻右边位置的右边界值加上1,下边界值为相邻下边位置的下边界值加上1。

hasSizeOfBorder函数中,该函数用于检查指定边长的正方形是否存在边框全是1。函数中使用双层循环遍历可能的左上角起点,在每个起点位置,判断右边界、下边界、右下角位置的值是否都大于等于size,若是,则存在边框大小为size的正方形。

最后,在getMaxSize函数中,该函数计算最大边长的边框全是1的正方形的边长大小。从最大的边长开始,依次判断是否存在边框大小为size的正方形,若存在则返回size,若遍历完仍未找到边框全是1的正方形,则返回0。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值