详解各类排序方式
排序算法
排序算法是算法中最基础,最实用的算法之一。简单来说,就是将一组无序数列按照设定的排序方式进行排列
练习程序
冒泡排序
冒泡排序是最简单的一种排序方式之一,重复循环该数列,然后一次比较两个元素,如果排序错误就将该两个元素进行位置交换,依次重复当前操作直到不可操作
依稀记得在刚学习Java的时候,大冬天在家里练习冒泡手冷的场景
我们先来看看动图效果:
依照上图的展示形式,其实我们得到如下思路
- 第一次循环的时候,元素比较进行交换,那么最终会将该组数列中最大的元素放到最后一位,此后数列循环范围是【0~N-2】
- 第二次循环的时候,元素比较进行交换,在最后将此时数列中最大的元素放到【N-1】的位置,此后数列循环范围是【0~N-3】
- ...
- 每次循环范围递减,直到最终的排序完成
【元素下标具体表示形式】
0和1比较 1和2比较 2和3比较 。。。end-1和end比较
那么明白了其中的思路之后,下面我们开始正式编码
// 元素交换, 在后面也用这个
public static void swap(int[] arr, int i, int j) {
int tep = arr[i];
arr[i] = arr[j];
arr[j] = tep;
}
public static void bubbetSort(int[] arr) {
// 如果是null或者元素只有一个,根本就没必要排序
if (null == arr || arr.length == 1) {
return;
}
// 第一层循环遍历
for (int i = 0, size = arr.length; i < size; i++) {
// 第二层循环遍历,用来两两元素进行比较
for (int j = 1; j < size; j++) {
if (arr[j] < arr[j - 1]) {
// 交换位置
swap(arr, j - 1, j);
}
}
}
}
到这里就完成了冒泡,大家可以自己测试
选择排序
选择排序在每次循环数列的时候会标记除当前数列中最小的元素,然后和第【0 + 1】的位置进行元素交换,这种情况下保证了最起始位置上一定是最小的元素
和冒泡排序唯一的区别就是:
- 冒泡固定最大值,数列的最后位置
- 选择固定最小值,数列的起始位置
还是先来看看动图效果
思路很重要:
- 第一次循环整个数列,找到该数列中最小的元素,然后将它和第一位上的元素进行交换,这样就固定了索引0上的元素,此后循环范围为【1~N-1】
- 第二次循环数列,再次找到数列中最小元素,然后和索引1上的元素进行交换。此后循环范围【2~N-1】
- 。。。
- 最后一次循环数列范围为【N-2~N-1】,比较然后进行交换
好,按照这样的逻辑,我们来看看代码实现
public static void choiceSort(int[] arr) {
// 如果数据为null或者元素只有1个,不处理
if (null == arr || arr.length == 1) {
return;
}
// 循环遍历,直到最后一个元素
for (int i = 0, size = arr.length; i < size; i++) {
// 定义最小值的数组下标
int minValueIndex = i;
// 依次循环数列,找到当前数列中最小元素的下标
for (int j = i+1; j < size; j++) {
minValueIndex =
arr[minValueIndex] > arr[j] ? j : minValueIndex;
}
// 将最小元素交换到指定位置
swap(arr, i, minValueIndex);
}
}
插入排序
插入排序也是一种简单直观的排序方式,在循环的时候会将元素和之前构建的有序数列依次进行比较,在哪里小就插入到对应的位置上
同理还是先来看看动图效果
如果用流程来表示的话就是这样的:
- 【0~0】范围内排序,保证了有序
- 【0
1】范围内排序,在第一步已经保证了【00】上有序,所以需要将索引1上的元素和之前的数列进行比较 - 【0
2】范围内排序,在上一步已经保证了【01】上有序,所以需要将索引2上的元素和之前的数列进行比较 - 。。。
- 【0
N-1】范围内排序,就需要将索引N-1上的元素和【0N-2】范围内的有序数列进行比较
思路已经有了,那么我们就开始Coding吧
public static void insertSort(int[] arr) {
// 如果数据为null或者元素只有1个,不处理
if (null == arr || arr.length == 1) {
return;
}
// 循环数列,索引0上只有1位元素,这个元素就不需要比较,所以循环的起始位置从1开始
for (int i = 1, size = arr.length; i < size; i++) {
int newValueIndex = i;
// 保证newValueIndex--的过程中不会为负数 && 当前元素小于前一个元素
while (newValueIndex > 0 && arr[newValueIndex] < arr[newValueIndex - 1]) {
// 元素交换
swap(arr, newValueIndex, newValueIndex - 1);
newValueIndex--;
}
}
}
// 这是另一种写法,和上面是一样的
public static void insert2Sort(int[] arr) {
if (null == arr || arr.length == 1) {
return;
}
for (int i = 1, size = arr.length; i < size; i++) {
for (int j = i; j > 0 && arr[j] < arr[j - 1]; j--) {
swap(arr, j, j - 1);
}
}
}
归并排序
这个排序是一个非常有意思的排序方式,在大数据层面上运用非常广泛,采用分治思想
原理就是将大的数组拆分成多个小数组,对小数组内进行排序,保证有序之后,进行合并操作。最终完成对大数组的排序过程。
示例动图如下
在图中可以很明显的看到某些节点上颜色变淡
这里对应的是每个小数组中的指针位置,当两个小数组【PL, PR】需要进行数据合并的时候,会发生如下情况
- 当
PL上的P1位置 <PR上的P2位置,那么将P1位置元素提取出来,P1++,否则提取P2位置元素,P2++ - 当
P1指针的位置指向了PL的最后一个元素,此时PL数组全部提取完成,那么如果PR还存在元素的情况下,那么就将PR剩下的元素原封不动提取出来,反之亦然 - 最后将合并之后的数组还原到原先的数组上去
原理将清楚之后,那么接下来就看看代码实现:
public static void process(int[] arr, int L, int R) {
// 如果只有一个元素, 就不排序
if (L == R) {
return;
}
// 分组 找到中间位置,将数组对半劈
int mid = L + ((R - L) >> 1);
// 递归左
process(arr, L, mid);
// 递归右
process(arr, mid + 1, R);
// 合并左右数组
merge(arr, L, mid, R);
}
public static void merge(int[] arr, int L, int mid, int R) {
int[] helper = new int[R - L + 1];
int i = 0;
int P1 = L;
int P2 = mid + 1;
// 如果两者都没有超过边界
while (P1 <= mid && P2 <= R) {
helper[i++] = arr[P1] <= arr[P2] ? arr[P1++] : arr[P2++];
}
// 如果P1没有超过边界,那么拷贝P1剩下的元素
while (P1 <= mid) {
helper[i++] = arr[P1++];
}
// 虽然这里写了两个循环,但是只会有一个地方执行
while (P2 <= R) {
helper[i++] = arr[P2++];
}
// copy到原始数组中
System.arraycopy(helper, 0, arr, L, helper.length);
}
// process(arr, 0, arr.length-1);
这是递归写法
非递归的写法就不展示了,主要就是变化在什么地方拆分数组,也就是步长,然后限制边界条件
快速排序
快速排序也是属于分治思想的产物,原理也非常简单:
- 取数组中的某一个数作为排序的基准
【P】,然后将待排序的数据中的元素和【P】进行比对,小的就放到左边,大的放到最右边,等于的放中间。 - 按照这种方式排序,中间位置一定是有序的,那么我们就只要处理左边和右边的即可
- 递归第一步,最终排序完成
我们来手动画一张图来看看这个思想
首先,我们先来看看分为两个区的过程
那么,如果采用代码的话就是如下程序
public static void splitNum(int[] arrs) {
int N = arrs.length;
int index = 0;
int lessM = -1;
while (index < N) {
if (arrs[index] <= arrs[N-1]) {
swap(arrs, ++lessM, index++);
} else {
index++;
}
}
}
public static void swap(int[] arrs, int i, int j) {
int tmp = arrs[i];
arrs[i] = arrs[j];
arrs[j] = tmp;
}
两个区的分完了,下面升级难度,看看如何将数组分为 <区,=区, >区
原理都是一样的,每个元素和基准值进行比对
- 如果小于基准值:将index上的元素和++less的位置进行交换,然后index++
- 如果等于基准值:不操作,直接index++
- 如果大于基准值,将--moreR的值和index的值进行交换
- 最后一步将基准值和moreR的位置进行交换
这种情况相当于一点点的将中间=区向中间挤,如图过程
代码也非常简单:
public static void splitNum2(int[] arrs) {
int N = arrs.length;
int index = 0;
int lessM = -1;
int moreR = N - 1;
while (index < moreR) {
if (arrs[index] < arrs[N - 1]) {
swap(arrs, ++lessM, index++);
} else if (arrs[index] > arrs[N - 1]) {
swap(arrs, --moreR, index);
} else {
index++;
}
}
swap(arrs, moreR, N-1);
}
为什么要++less, 因为less是从-1开始的
为什么要--moreR,因为moreR的起始位置是N-1的位置
至于为什么要做上面的操作,快速排序本身基于分三区的过程来操作,专业一点可以讲上面的过程成为分区,那么接下来才是快速排序的正式操作
首先需要改变一下splitNum2()使它返回=区的开始和结束位置,这样我们才能做后续操作
// L = 0, R = arrs.length-1 来考虑
public static int[] partition(int[] arrs, int L, int R) {
int lessM = L - 1;
int moreR = R;
int index = L;
while (index < moreR) {
if (arrs[index] < arrs[R]) {
swap(arrs, ++lessM, index++);
} else if (arrs[index] > arrs[R]) {
swap(arrs, --moreR, index);
} else {
index++;
}
}
swap(arrs, index, R);
return new int[]{lessM, moreR};
}
// 主方法
public static void quickSort(int[] arrs) {
if (arrs == null || arrs.length < 2) {
return;
}
// 递归方法
process(arrs, 0, arrs.length - 1);
}
public static void process(int[] arrs, int L, int R) {
// 如果L > R,不符合条件
if (L >= R) {
return;
}
// 分区交换
int[] partition = partition(arrs, L, R);
// 递归左部分
process(arrs, L, partition[0] - 1);
// 递归右部分
process(arrs, partition[1] + 1, R);
}
总的来说,快速排序的代码量相对比上面4个排序多了不少,还是按照流程来看一看,练一练
二分法
对半劈:小了找左边,大了找右边
一般情况下使用二分法的数组都是基于有序状态的,这样才能通过对半劈的方式来找到我们想要的东西
在LinkedList中的
get(int index)方法就是通过二分法来优化了检索的过程
练习程序
概念上的东西实在不知道该说什么,我们还是来做题吧
找到目标所在的索引
给定一组有序的数组和一个目标值,返回其所在的索引位置,如果在当前数组中不存在,那么就返回-1
比如:
参数:arr = [1,2,3,6,7,8,9] target = 2
返回:index=1
参数:arr = [1,2,3,6,7,8,9] target = 22
返回:index=-1
好,我们明白了题目之后,就来看看如何对半:
-
既然需要对半,那么我们就将数组长度 / 2, 得到一个位置,然后该中间位置的值和目标值进行比对:
- 如果中间值 < 目标值,那么目标值就可能在右半边
- 如果中间值 > 目标值,那么目标值就可能在左半边
-
重复上面的过程,直到找到目标值的位置
public static int findByTarget(int[] arrs, int target) {
// 为空不分
if (null == arrs || arrs.length == 0) {
return -1;
}
// 只有一个元素, 就单独判断
if (arrs.length == 1) {
return arrs[0] == target ? 0 : -1;
}
// 左右边界,将目标锁定在某一段范围内
int left = 0;
int right = arrs.length;
while (left <= right) {
int middle = (left + right) >> 1;
if (arrs[middle] == target) {
return middle;
}
// 如果中间值大于目标值,说明在左边,右边界减1
if (arrs[middle] > target) {
right = middle -1;
} else {
// 如果中间值小于目标值,说明在右边,左边界加1
left = middle + 1;
}
}
return -1;
}
找满足>=value的最左位置
一组数组和一个给定值,找到在数组中>=目标值的最左的位置
参数:arr = [1,2,2,3,3,5,6,6,7,8,9] target = 3
返回:index=3
原则上思路和上面那道题是一样的,还是直接来看代码吧
public static int lessLeft(int[] arrs, int target) {
// 为空不分
if (null == arrs || arrs.length == 0) {
return -1;
}
// 只有一个元素, 就单独判断
if (arrs.length == 1) {
return arrs[0] >= target ? 0 : -1;
}
int L = 0;
int R = arrs.length;
int index = -1;
while (L <= R) {
int middle = (L + R) >> 1;
if (arrs[middle] >= target) {
// 这里不直接返回:虽然找到了一个符合条件的,但是不能确定这是唯一一个符合条件的,所以需要继续执行
index = middle;
R = middle - 1;
} else {
L = middle + 1;
}
}
return index;
}
找满足<=value的最右位置
public static int lessTarget(int[] arrs, int target) {
// 为空不分
if (null == arrs || arrs.length == 0) {
return -1;
}
// 只有一个元素, 就单独判断
if (arrs.length == 1) {
return arrs[0] <= target ? 0 : -1;
}
int L = 0;
int R = arrs.length;
int index = -1;
while (L <= R) {
int middle = (L + R) >> 1;
if (arrs[middle] <= target) {
index = middle;
L = middle + 1;
} else {
R = middle - 1;
}
}
return index;
}
这里和上一题就是一个模子里出来的,就不多说什么了
这里强调一点:在做二分法题目的时候只需要记住这一点就好
- 当中间值 > 目标值的时候,调整右边界
- 当中间值 < 目标值的时候,调整左边界
只要按照对半分的思想,这样在做的时候就非常简单了
找到数组中局部最小的位置
上面我们练习的数组都是有序数组,那么还有一种情况下是可以不要求数组有序的,如题:
在一个数组中,相邻的两个数不一样,要求要找出整个数组中局部最小的位置
先思考一下
根据这个题意我们可以提炼出以下的思路:
- 还是取中间位置,由于相邻的两个数不一样,那么只要能够保证
middle比middle - 1和middle + 1位置上的数都小,那么middle上的就是局部最小的 - 否则的话就对半劈
那么就按照这个思路,我们来看看代码如何实现:
public static int findLocalMinimum(int[] arrs) {
if (null == arrs || arrs.length == 0) {
return -1;
}
int N = arrs.length;
// 这里判断了只有一位的时候
if (N == 1) {
return 0;
}
// 这里判断只有两位的时候
// 0和1比较
if (arrs[0] < arrs[1]) {
return 0;
}
// 1和0比较
if (arrs[N - 1] < arrs[N - 2]) {
return N - 1;
}
// 大于2位的情况
int L = 0;
int R = N - 1;
while (L < R - 1) {
int middle = (L + R) >> 1;
// 我们要找的不是数组中最小的位置,而是有局部最小的位置
if (arrs[middle] < arrs[middle -1] && arrs[middle] < arrs[middle + 1]) {
// 局部最小
return middle;
} else {
// 如果是中间位置大,左边位置小,说明整体线是向下延伸的,那么就将右边抛弃,只留左边
if (arrs[middle] > arrs[middle - 1]) {
R = middle - 1;
} else {
// 将左边抛弃,只留右边
L = middle + 1;
}
}
}
// 从(L < R - 1)出来,说明当前L和R的区间内只存在两个元素,那么只需要单独对这两个元素进行判断就行
return arrs[L] < arrs[R] ? L : R;
}
难点:while条件是L < R - 1而不是L <= R
如果while条件是L <= R的话,因为在内部需要判断middle+1和middle-1的位置,如果L和R正好相等,那么就会触发边界问题,有可能抛出异常
而L < R - 1这样就能避免L和R正好相等的情况
最后
为大家推荐一个学习算法的网站,该文中的动图都是从该网站录制出来的