做练习前,请先看 十大经典排序算法(详解)

1、调整数组顺序使奇数位于偶数前面

题目:

  调整指定数组中数字的顺序,使得所有奇数位于数组的前半部分,所有偶数位于数组的后半部分,要求时间复杂度为 O(n)。

思路:

  和快排的思路相似,使用单向扫描法或双向扫描法,判断数的奇偶性然后进行换位。

代码实现:

    // 单向扫描法,也可以使用双向扫描法
    public static void practice(int[] array) {
        int left = 0;
        int right = array.length - 1;
        int temp;
        while (left < right) {
            // 对 2 求余不位 0 则为奇数
            if (array[left] % 2 != 0) {
                left++;
            // 如果为偶数则将该数放后面
            } else {
                temp = array[left];
                array[left] = array[right];
                array[right] = temp;
                right--;
            }
        }
    }

2、第 k 个元素

题目:

  以尽量高的效率求出一个乱序数组中按数值顺序的第 k 个元素值。

思路:

  和快排的解法相似,每次递归判断该主元素的下标,来进行折半查找。

代码实现:

    public static int practice(int[] array, int left, int right, int k) {
        // 主元素的下标
        int midn = median(array, left, right);
        if (midn == k) {
            return array[midn];
        } else if (midn > k) {
            return practice(array, left, midn - 1, k);
        } else {
            return practice(array, midn + 1, right, k);
        }
    }

    public static int median(int[] array, int left, int right) {
        int mainElement = array[left];
        int start = left + 1;
        int end = right;
        while (start <= end) {
            while (start <= end && array[start] <= mainElement) {
                start++;
            }
            while (start <= end && array[end] >= mainElement) {
                end--;
            }
            if (start < end) {
                int temp = array[start];
                array[start] = array[end];
                array[end] = temp;
            }
        }
        int temp = array[left];
        array[left] = array[end];
        array[end] = temp;
        return end;
    }

3、超过一半的数字

题目:

  数组中有一个数字出现的次数超过了数组长度的一半,找出这个数字。

思路:

  • 解法一: 排序后返回 array[array.length/2],时间复杂度 O(nlogn)。
  • 解法二:直接使用上个题的代码得知下标为 array.length/2 的元素即可,O(n),需要更改数组内容。
  • 解法三:利用数组的特性,一个数超过数组的一半则该数出现的次数比其他数的总次数还要多。创建两个变量,一个存储当前数,一个存储当前数的个数,遍历数组,不同则消去相同则相加,具体看代码。

代码实现:

    // 解法三:
    public static int method(int[] array) {
        int num = array[0];
        int count = 1;
        // 因为下标为0的数,已经当做变量的初值了,所以从1开始循环
        for (int i = 1; i < array.length; i++) {
            // count等于0表示该数已经被消去,重新记录新的数
            if (count == 0) {
                num = array[i];
                count = 1;
            } else {
                // 相同计数增加
                if (num == array[i]) {
                    count++;
                } else {
                    count--;
                }
            }
        }
        // 由题意可知:一个数超过数组的一半则该数出现的次数比其他数的总次数还要多,所以剩下的肯定是那个数
        return num;
    }

加强:

  如果该数出现的个数正好是数组的一半,这么得到该数。

思路:

  由题意得该数组是一个偶数个数,所以使用上面的解法只有当最后一个数是那个数的时候才可能出错,因此我们只需要对最后一个数单独计数即可。

代码实现:

    public static int method(int[] array) {
        int num = array[0];
        int count = 0;
        // 对于最后一个数单独计数
        int endCount = 0;
        // 数组的长度
        int length=array.length;
        // 因为所有数都需要比较,所以从0开始
        for (int i = 0; i < length; i++) {
            if (array[i] == array[length - 1]) {
                endCount++;
            }
            if (count == 0) {
                num = array[i];
                count = 1;
            } else {
                if (num == array[i]) {
                    count++;
                } else {
                    count--;
                }
            }
        }
        // 如果最后一个数出现的次数等于数组长度的一半,则最后一个数肯定是那个数。
        if (endCount == length / 2) {
            return array[length - 1];
        } else {
            return num;
        }
    }

4、最小可用 ID

题目:

  在非负数组(乱序)中找到最小的可分配 ID(从 1 开始编号)。例如:{1,5,6,3,8,7,2},最小的可分配 ID 为 4。

思路:

  • 解法一:暴力解法从 1 开始查找每个自然数是否在数组中,时间复杂度 O(n2)。
  • 解法二:先将数组排序,然后在遍历,时间复杂度 O(nlogn)。
  • 解法三:创建一个临时数组长度为指定数组的长度加一,遍历指定数组将数组中的数按值赋值给临时数组的指定下标,遍历临时数组查看那个下标为被赋值即可。
  • 解法四:使用单向扫描或双向扫描,确定主元素在排序后的下标。然后利用数组的特性,判断主元素是否等于当前下标加一,如果相等则表明可分配 ID 在主元素的右边,如果不相等则表示可分配 ID 在主元素的左边,递归调用即可查找最小可分配 ID。

代码实现:

解法三:

    // 解法三
    public static int method(int[] array) {
        int length = array.length;
        // 因为是从1开始编码的,长度需要加1才不会越界
        int[] copyArray = new int[length + 1];
        for (int i = 0; i < array.length; i++) {
            if (array[i] < length + 1) {
                copyArray[array[i]] = 1;
            }
        }
        for (int i = 1; i <= length; i++) {
            if (copyArray[i] != 0) {
                return i;
            }
        }
        return length + 1;
    }

解法四

    // 解法四
    public static int method(int[] array, int left, int right) {
        if (left <= right) {
            // median方法是一个双向扫描查找主元素的下标
            int mid = median(array, left, right);
            // 如果相等表示左稠密,去右边查找
            if (mid + 1 == array[mid]) {
                return method(array, mid + 1, right);
            } else {
                return method(array, left, mid - 1);
            }
        } else {
            // 因为下标从0开始,所以加一
            return left + 1;
        }
    }

    public static int median(int[] array, int left, int right) {
        int mainElement = array[left];
        int start = left + 1;
        int end = right;
        while (start <= end) {
            while (start <= end && array[start] <= mainElement) {
                start++;
            }
            while (start <= end && array[end] >= mainElement) {
                end--;
            }
            if (start < end) {
                int temp = array[start];
                array[start] = array[end];
                array[end] = temp;
            }
        }
        int temp = array[left];
        array[left] = array[end];
        array[end] = temp;
        return end;
    }

5、合并有序数组

题目:

  给定两个排序后的数组 A 和 B,以及 A 数组元素的个数和 B 数组元素的个数,其中 A 的末端有足够的缓冲空间容纳 B。编写一个方法将 B 合并入 A 并排序。

思路:

  • 解法一:暴力解法,先将 B 数组拷贝到 A 数组中然后排序。
  • 解法二:创建两个指针分别指向 a 数组元素的末尾和 b 数组元素的末尾,同时遍历 a 、b 两个数组,将大的元素放到数组 a 的末尾即可,最后如果 b 数组没有遍历完,则将剩下的元素合入到 a 数组即可。

代码实现:

    // 解法二,m 表示 a 数组元素的个数,n 表示 b 数组元素的个数
    public void merge(int[] a, int m, int[] b, int n) {
        int pointer = a.length - 1;
        int pointera = m - 1;
        int pointerb = n - 1;
        while (pointera >= 0 && pointerb >= 0) {
            if (a[pointera] >= b[pointerb]) {
                a[pointer--] = a[pointera--];
            } else {
                a[pointer--] = b[pointerb--];
            }
        }
        if (pointerb >= 0) {
            for (int i = pointerb; i >= 0; i--) {
                a[pointer--] = b[i];
            }
        }
    }

6、逆序对个数

题目:

  一个数列,如果左边的数大,右边的数小,则称这两个数为一个逆序对。求出一个数列中有多少个逆序对。

思路:

  直接使用归并排序,在数组合并时,如果左边元素大于右边元素,则表明该右边这个元素能和左边的元素组成 mid - leftPointer + 1个逆序对。

代码实现:

    // 统计逆序对的个数
    static int count = 0;
  
    // 第一次调用 left为 0 ,right为 array.length - 1
    public static void sort(int[] array, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            // 对左边进行排序
            sort(array, left, mid);
            // 对右边进行排序
            sort(array, mid + 1, right);
            // 合并
            merge(array, left, mid, right);
        }
    }

    public static void merge(int[] array, int left, int mid, int right) {
        // 创建一个辅助的临时数组
        int[] tempArray = new int[right - left + 1];
        // 记录临时数组的下标
        int tempIndex = 0;
        // 左边的指针
        int leftPointer = left;
        // 右边的指针
        int rightPointer = mid + 1;
        while (leftPointer <= mid && rightPointer <= right) {
            // 如果左边的元素小于等于右边的元素,则将左边的元素,存入临时数组,否则相反
            if (array[leftPointer] <= array[rightPointer]) {
                tempArray[tempIndex++] = array[leftPointer++];
            } else {
                tempArray[tempIndex++] = array[rightPointer++];
                count += mid - leftPointer + 1;
            }
        }
        // 将左序列剩下的元素,拷贝到临时数组中
        while (leftPointer <= mid) {
            tempArray[tempIndex++] = array[leftPointer++];
        }
        // 将右序列剩下的元素,拷贝到临时数组中
        while (rightPointer <= right) {
            tempArray[tempIndex++] = array[rightPointer++];
        }
        // 将临时数组拷贝到原数组中
        System.arraycopy(tempArray, 0, array, left, tempArray.length);
    }

7、排序数组中找和的因子

题目:

  给定已排序的数组 arr 和 k,不重复打印 array 中所有相加和为 k 的不降序二元组。

例如:

 输入:array = {-8, -4, -3, 0, 2, 4, 5, 8, 9, 10},k = 10
 输出:[(0,10), (2,8)]

思路:

  以例子为例,-8 是数组中最小的数,如果想凑成 k,需要一个较大的数。所以我们创建俩个指针,左指针指向-8,右指针指向 10,如果 -8 + 10 大于 10 则移动右指针,如果小了则移动左指针。

代码实现:

    public static List<String> practice(int[] array, int k) {
        List<String> list = new ArrayList<>();
        int left = 0;
        int right = array.length-1;
        while (left < right) {
            if ((array[left] + array[right]) < k) {
                left++;
            } else if ((array[left] + array[right]) > k) {
                right--;
            } else {
                list.add("(" + array[left] + "," + array[right] + ")");
                left++;
                right--;
            }
        }
        return list;
    }

8、最短无序连续子数组

题目:

  给定一个整数数组,你需要寻找一个连续的子数组,如果对这个子数组进行升序排序,那么整个数组都会变为升序排序,你找到的子数组应是最短的,请输出它的长度。要求:O(n)。

例如:

 输入:array = {2, 3, 7, 5, 4, 6, 8}
 输出:4,因为只有{7, 5, 4, 6}需要排序。

思路:

  在升序数组中,右边的数总是大于左边的数。所以判断数组无序的范围是:
如果左边存在一个数大于右边最小的数,则该数的下标是结果数组的起点。如果右边存在一个数小于左边最大的数,则该数的下标是结果数组的终点。

代码实现:

    public static int practice(int[] array) {
        int length = array.length;
        int max = array[0];
        int min = array[length - 1];
        // 左指针和右指针
        int left = 0, right = -1;
        for (int i = 0; i < length; i++) {
            // 从左往右开始遍历,如果发现右边的数小于左边最大的数,则记录下来
            if (max > array[i]) {
                right = i;
            } else {
                // 记录左边最大的数
                max = array[i];
            }
            // 从右往左开始遍历,如果发现左边的大于右边最小的数,则记录下来
            if (min < array[length - i - 1]) {
                left = length - i - 1;
            } else {
                // 记录右边最小的数
                min = array[length - i - 1];
            }
        }
        return right - left + 1;
    }

9、前 k 个数

题目:

  求海量数据(正整数)按逆序排列的前 k 个数(topK),因为数据量太大,不能全部存储在内存中,只能一个一个地从磁盘或者网络上读取数据,请设计一个高效的算法来解决这个问题。
  用户先输入 K,代表要求得的 topK,随后的输入 N 个数,输入-1 代表输入终止。请输出 topK,从小到大,空格分割。

思路:

  所谓 topK 就是只维护数值大的数,用户每输入一个数和该数组中最小的数比较,大的话则交换,小的话则舍去。可以利用小顶堆的性质,每次将输入的数和推顶进行比较,如果交换的话则再次将该数组堆化即可。最后利用堆排序将数组逆序打印即可。

代码实现:

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 输入topK
        int k = scanner.nextInt();
        // 创建一个小顶堆
        int[] heap = new int[k];
        int value = scanner.nextInt();
        // 只要用户输入的数不为-1则一直执行
        while (value != -1) {
            if (count < k) {
                // 如果个数小于堆的容量,则直接存储
                heap[count] = value;
                count++;
                if (count == k) {
                    // 如果加满,则将数组堆化
                    pile(heap, heap.length);
                }
            } else {
                if (value > heap[0]) {
                    // 如果大于,则将和推顶元素互换,再进行堆化
                    heap[0] = value;
                    pile(heap, heap.length);
                }
            }
            // 获取用户输入的数
            value = scanner.nextInt();
        }
        // 输出结果
        for (int i = heap.length; i > 1; i--) {
            System.out.print(heap[0] + " ");
            heap[0] = heap[i - 1];
            pile(heap, i);
        }
        System.out.print(heap[0] + " ");
    }


    // 将数组小顶堆化,第一次length参数传入 array.length
    public static void pile(int[] array, int length) {
        // array.length/2-1得到最小的有子节点的节点
        for (int i = length / 2 - 1; i >= 0; i--) {
            smallTopPile(array, i, length);
        }
    }

    // 将指定堆转换为小顶堆
    public static void smallTopPile(int[] array, int root, int length) {
        int left = root * 2 + 1;
        int right = root * 2 + 2;
        // 判断是否有左节点
        if (left >= length) {
            return;
        }
        // 用于存储最小节点的下标
        int min;
        // 如果右节点存在且小于左节点则执行
        if (right < length && array[right] < array[left]) {
            min = right;
        } else {
            min = left;
        }
        // 如果父节点,小于等于子节点则结束递归,否则进行交换并继续向下递归
        if (array[root] <= array[min]) {
            return;
        } else {
            int temp = array[root];
            array[root] = array[min];
            array[min] = temp;
        }
        smallTopPile(array, min, length);
    }

10、数组能排成的最小数(特殊排序)

题目:

  输入一个正整数数组,把数组里所有整数拼接起来排成一个数,打印出能拼接的所有数字中最小的一个。

例如:

 输入:array = {3, 32, 321},则打印出这 3 个数字能排成的最小数字。
 输出:321323

思路:

  这个题可以理解为对字符串进行升序排序,当字符串 "3" 加 "32" 大于 "32" 加 "3" 时,3 放在 32 的后面,否则相反。

代码实现:

    public static int practice(Integer[] array) {
        Arrays.sort(array, new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                // 设置排序规则
                String s1 = o1 + "" + o2;
                String s2 = o2 + "" + o1;
                return s1.compareTo(s2);
            }
        });
        StringBuilder stringBuilder = new StringBuilder();
        for (Integer integer : array) {
            stringBuilder.append(integer);
        }
        return Integer.parseInt(stringBuilder.toString());
    }

标题:关于排序的练习题
作者:Yi-Xing
地址:http://47.94.239.232/articles/2019/11/18/1574088001295.html
博客中若有不恰当的地方,请您一定要告诉我。前路崎岖,望我们可以互相帮助,并肩前行!