[算法入门笔记] 2. 排序算法的简单应用

本篇主要针对常见排序算法整合简单应用场景,在日常刷题总结过程中学会利用排序思想,如有不足,恳请指正,欢迎交流~


前言

在日常刷题过程中将用到的排序算法思想总结至此,有助于拓展解题思维~

1. 小和问题 [归并排序思想]

在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。求一个数组的小和。

[1, 3, 4, 2, 5]
1 左边比 1 小的数,没有;
3 左边比 3 小的数,1;
4 左边比 4 小的数,1、3;
2 左边比 2 小的数,1;
5 左边比 5 小的数,1、3、4、 2;
因此小和为 1 + 1 + 3 + 1 + 1 + 3 + 4 + 2 = 16

图解算法
1/3
在这里插入图片描述
2/3
在这里插入图片描述
3/3
在这里插入图片描述
代码实现

merge

  public static int merge(int[] arr, int left, int mid, int right) {
        int[] temp = new int[right - left + 1];
        int i = 0;
        int p1 = left;
        int p2 = mid + 1;
        int res = 0;
        while (p1 <= mid && p2 <= right) {
            //求小和
            res += arr[p1] < arr[p2] ? (right - p2 + 1) * arr[p1] : 0;
            //归并
            temp[i++] = arr[p1] < arr[p2] ? arr[p1++] : arr[p2++];
        }
        //当左边元素有剩余
        while (p1 <= mid) {
            temp[i++] = arr[p1++];
        }
        //当右边元素有剩余
        while (p2 <= right) {
            temp[i++] = arr[p2++];
        }
        //重新拷贝回原始数组对应位置
        for (int j = 0; j < temp.length ; j++) {
            arr[left + j] = temp[j];
        }
        return res;
    }

mergeSort

 public static int mergeSort(int[] arr, int left, int right) {
        if (left == right) {
            return 0;
        }
        int mid = left + ((right - left) >> 1);
        return mergeSort(arr, left, mid)
                + mergeSort(arr, mid + 1, right)
                + merge(arr, left, mid, right);
    }

smallSum

  public static int smallSum(int[] arr) {
        if (arr == null || arr.length < 2) {
            return 0;
        }
        return mergeSort(arr, 0, arr.length - 1);
    }

2. 逆序对问题 [归并排序思想]

算法思想

把要统计逆序对个数的序列 C 沿着中间位置切分成两个子序列 A 和 B,递归地计算子序列 A 和子序列 B 中逆序对的个数并排序,然后合并两个子序列,合并的同时计算所有 (ai, aj) 的数对中逆序对的个数 ( ai 在子序列 A,aj 在子序列 B )。

由于子序列 A 和 B 是已经排好序的,在把 A 和 B 合并到 C 时,按照归并排序的合并过程,每次都挑选两个子序列中最小的元素加入到 C 中。

每次 A 中的元素 ai 被加到 C 中,不会遇到新的逆序,因为 ai 小于子序列 B 中剩下的每个元素,并且 ai 出现在 B 中元素的前面。

但每次 B 中的元素 bj 被加到C中,说明它比子序列 A 中剩下的元素都小由于 B 中所有元素本来都排在 A 后面,所以 bj 就与 A 中剩下的所有元素都构成逆序对,此时 A 中剩下的元素个数就是与 bj 构成的逆序对的个数。

图解算法
1/2
在这里插入图片描述

2/2
在这里插入图片描述
伪代码实现

mergeAndCount(A, B)

初始化count= 0C =while AB都不为空
  令ai和bj分别为AB中的首元素
  if ai <= bj
    把ai加入到输出表CA = A - {ai}
  else
    把bj加入到输出表CB = B - {bj}
    count += A中剩下的元素
  endIf
endWhile
if A 为空
  把B中剩下元素加入到C
elseA中剩下元素加入到C
endIf
return 合并结果C和逆序对个数count

sortAndCount ( C )

if L只有1个元素
  没有逆序,c1 = c2 = c3 = 0
else
  把这个表C均分成两半,AB
  (c1, A) = sortAndCount(A)
  (c2, B) = sortAndCount(B)
  (c3, C) = mergeAndCount(A, B)
endIf
return (c1 + c2 + c3, C)

代码实现

merge

    /**
     * 归并同时求逆序对
     * @param arr
     * @param left
     * @param mid
     * @param right
     * @return 逆序对个数
     */
    public static int merge(int[] arr, int left, int mid,int right) {
        int[] temp =new int[right - left + 1];
        int i = left, j = mid + 1, k = 0, count = 0;
        while (i <=mid && j <= right) {
            //从mid开始划分成两段数组进行归并并找出逆序对
            if (arr[i] <= arr[j]) {
                temp[k++] = arr[i++];
            } else { //当B段小于A段的数时,便找到了逆序对
                temp[k++] = arr[j++];
                //此时逆序对数为A段剩余元素数量
                count += mid - i + 1;
            }
        }
        while (i <= mid) {
            temp[k++] = arr[i++];
        }
        while (j <= right) {
            temp[k++] = arr[j++];
        }
        for (i = 0; i < temp.length; i++) {
            arr[left + i] = temp[i];
        }
        return count;
    }

mergeSort

    /**
     * 对于任意的边界L:R求出逆序对
     * @param arr
     * @param left
     * @param right
     * @return
     */
    public static int mergeSort(int[] arr, int left, int right) {
        if (left == right) {
           return 0;
       }
        int mid = left + ((right - left) >> 1);
        return mergeSort(arr, left, mid) +
               mergeSort(arr, mid + 1, right) +
                merge(arr, left, mid, right);
    }

reverseSort

public static int reverseSort(int[] arr) {
        if (arr == null || arr.length < 1) {
            return 0;
        }
        return mergeSort(arr,0,arr.length-1);
    }

3. 堆排序扩展 [堆排序思想]

已知一个几乎有序的数组,几乎有序是指,如果把数组排好顺序的话,每个元素移动的距离可以不超过k,并且k相对于数组来说比较小。请选择一个合适的 排序算法针对这个数据进行排序 。

算法图解
在这里插入图片描述
代码实现

sortedArrDistanceLessK

 public void sortedArrDistanceLessK(int[] arr, int k) {
        PriorityQueue<Integer> heap = new PriorityQueue<>();
        int index = 0;
        for (; index < Math.min(arr.length, k); index++) {
            heap.add(arr[index]);
        }
        int i = 0;
        for (; index < arr.length; i++, index++) {
            heap.add(arr[index]);
            arr[i] = heap.poll();
        }
        while (!heap.isEmpty()) {
            arr[i++] = heap.poll();
        }
    }

4. 荷兰旗问题 [快速排序思想]

枢轴问题

给定一个数组 arr,和一个数 num,请把小于等于 num 的数放在数组的左边,大于 num 的 数放在数组的右边。要求额外空间复杂度O(1),时间复杂度O(N)。

算法图解
在这里插入图片描述
代码实现

partition

    public static void partition(int[] arr, int left, int right, int target) {
       //小于小等于边界
        int j = left - 1;
        int i = 0;
        while (i < arr.length) {
            if (arr[i] <= target) {
                utils.swap(arr,++j,i++);
            } else {
                i++;
            }
        }
    }

荷兰国旗问题

给定一个数组 arr,和一个数 num,请把小于 num 的数放在数组的 左边,等于 num 的数放在数组的中间,大于 num 的数放在数组的右边。要求额外空间复杂度O(1),时间复杂度 O(N)。
算法图解
在这里插入图片描述
代码实现

Netherlands

public static int[] Netherlands(int[] arr, int left, int right, int target) {
        int i = left - 1;
        int k = right + 1;
        while (left < k) {
            if (arr[left] < target) {
                utils.swap(arr, ++i, left++);
            } else if (arr[left] > target) {
                utils.swap(arr, --k, left);
            } else {
                left++;
            }
        }
        //target边界
        return new int[] {i + 1, k - 1};
    }

[补充] 二分法应用

在一个有序数组中,找某个数是否存在

代码实现

public static boolean binarySearch(int[] sortedArr, int num) {
    if (sortedArr == null || sortedArr.length == 0) {
        return false;
    }
    int left = 0;
    int right = sortedArr.length - 1;
    int mid = 0;
    while (left < right) {
        //防止越界
        mid = left + ((right - left) >> 1);
        if (sortedArr[mid] == num) {
            return true;
        } else if (sortedArr[mid] > num) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return sortedArr[left] == num;
}

在一个有序数组中,找大于等于某个数最左侧的位置 [重要]

代码实现

public static int nearestIndex(int[] arr,int value) {
    int left = 0;
    int right = arr.length - 1;
    int mid = 0;
    int index = -1;

    while (left < right) {
        mid = left + ((right - left) >> 1);
        if (arr[mid] >= value) {
            index = mid;
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return index;
}

局部最小值问题

在这里插入图片描述

情况3

mid比它左边小,比它右边小,所以已经是局部最小,直接返回

情况4

此时arr[0]~arr[1]是下降趋势, arr[mid-1]~arr[mid]是上升趋势,所以在此范围内数组arr一定存在局部最小。去掉mid右边的范围,直接找mid左边。

备注:虽然mid右边也存在局部最小,但是我们只需要返回一个局部最小,所以我们选择直接往mid左边找就可以了。

情况5

此时arr[mid]~arr[mid+1]是下降趋势, arr[n-2]~arr[n-1]是上升趋势,所以在此范围内数组arr一定存在局部最小。去掉mid左边的范围,找mid右边。

情况6

此时arr[0]~arr[1]是下降趋势, arr[mid-1]~arr[mid]是上升趋势,所以在此范围内数组arr一定存在局部最小。去掉mid右边的范围,找mid左边。

代码实现

public static int LocalMin(int[] arr) {
   //int ans = 0;
   //非法情况
   if (arr == null || arr.length == 0) {
       return -1;
   }

   int N = arr.length;
   if (N == 1) {
       return 0;
   }
   //情况1
   if (arr[0] < arr[1]) {
       return 0;
   }
   //情况2
   if (arr[N - 1] < arr[N - 2]) {
       return N - 1;
   }
   int left = 0;
   int right = N - 1;

   /**
    * L:R一定有局部最小
    * 保证L~R之间至少有三个数,才能二分
    * 若不足三个数直接比较大小
    */
   while (left < right) {
       int mid = left + ((right - left) >> 1);
       if (arr[mid] <= arr[mid - 1] && arr[mid] <= arr[mid + 1]) {
           return arr[mid];
       } else if (arr[mid] >= arr[mid + 1]) {
           left = mid + 1;
       } else {
           right = mid - 1;
       }
   }
   return -999;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyan Chau

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值