定义:
排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。平时的上下文中,如果提到排序,通常指的是排升序(非降序)。通常意义上的排序,都是指的原地排序(in place sort),就是在原有数组的基础上进行排序,而不是创建一个新的数组!
什么是稳定性?
两个相等的数据,如果经过排序后,排序算法能保证其相对位置不发生变化,则我们称该算法是具备稳定性的排序算法。
七大基于比较的排序
接下来,我们依次详细介绍一下这些排序算法~~
1. 直接插入排序
算法描述:
import java.util.Arrays;
public class MySort {
public static void StraightInsertionSort(int[] arr){
int bound = 1;
//[0,bound)是已排序区间,[bound,length)是待排序区间!
//接下来是要执行具体的比较插入过程!
for (;bound < arr.length;bound++){
//首先,取出待排序区间的最开始元素。
int val = arr[bound];
int cur = bound - 1;//cur指向排序区间最后一个位置!
//接下来的操作是执行比较插入的细节!
for (;cur >= 0;cur--){
//拿val这个值依次进行比较,直至找到合适位置
if (arr[cur] > val){
//说明val应该插到arr[cur]之前
//于是就需要将cur的位置往后搬运一个位置。
arr[cur + 1] = arr[cur];
}else{
break;
}
}
arr[cur + 1] = val;
}
}
public static void main(String[] args) {
int[] arr = {9, 5, 2, 7, 3, 6, 8};
System.out.println("原数组:"+Arrays.toString(arr));
StraightInsertionSort(arr);
System.out.println("直接插入排序处理后的数组:"+Arrays.toString(arr));
}
}
基本思想:
折半插入算法是对直接插入排序算法的改进,排序原理同直接插入算法:
把n个待排序的元素看成一个有序表和一个无序表,开始时有序表中只有一个元素,无序表中有n-1个元素;排序过程即每次从无序表中取出第一个元素,将它插入到有序表中,使之成为新的有序表,重复n-1次完成整个排序过程。
与直接插入算法的区别在于:在有序表中寻找待排序数据的正确位置时,使用了折半查找(二分查找)。这样下来,我们几乎可以节省一半的时间开销。
代码实现:
package Homework0323;
public class BinaryInsertSortTest {
public static void main(String[] args) {
int[] data = new int[] { 5, 3, 6, 2, 1, 9, 4, 8, 7 };
System.out.println("原始数组:");
print(data);
System.out.println("折半插入排序处理过程:");
binaryInsertSort(data);
System.out.println("折半插入排序处理后的数组:");
print(data);
}
public static void binaryInsertSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int val = arr[i];
int left = 0;
int right = i;
//[left,right)表示搜索区间,该区域是有序的。
while (left < right) {
int mid = (left + right) / 2;//记录搜索区域中间位置
if (val >= arr[mid]) {
left = mid + 1; //此处用到了二分查找的思想
} else {
right = mid;
}
}
//接下来将left-i处的数据整体后移一位
for (int j = i; j > left; j--) {
arr[j] = arr[j - 1];
}
arr[left] = val;
print(arr);
}
}
public static void print(int[] data) {
for (int j = 0; j < data.length; j++) {
System.out.print(data[j] + "\t");
}
System.out.println();
}
}
2.希尔排序
算法描述:
总的来说,希尔排序是针对插入排序的进一步改进~~
希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工作。当到达gap=1时,所有记录在统一组内排好序。
算法动态演示:
代码实现:
import java.util.Arrays;
public class MySort {
public static void ShellSort(int[] arr) {
//指定gap序列,一般取len/2,len/4,len/8……1。
int gap = arr.length / 2;
while (gap >= 1) {
_shellSort(arr, gap);//定义一个辅助方法具体实现希尔排序
gap = gap / 2;
}
}
public static void _shellSort(int[] arr, int gap) {
// 这个函数主要是进行分组插排. 分组依据就是 gap,gap 同时也表示分的组数.
// 同组的相邻元素, 下标差值就是 gap
// 下面的代码其实和插入排序是一样的. 尤其是把 gap 设为 1,那就完全一样!
int bound = gap;
for (; bound < arr.length; bound++) {
int val = arr[bound];
int cur = bound - gap;
for (; cur >= 0; cur -= gap) {
if (arr[cur] > val) {
// 进行搬运
arr[cur + gap] = arr[cur];
} else {
break;
}
}
arr[cur + gap] = val;
}
}
public static void main(String[] args) {
int[] arr2 = {84,83,87,61,50,70,60,80,99};
System.out.println("原数组:"+Arrays.toString(arr2));
ShellSort(arr2);
System.out.println("希尔排序处理后的数组:"+Arrays.toString(arr2));
}
}
1.选择排序
算法描述:
第一个跟后面的所有数相比,如果小于(或小于)第一个数的时候,暂存较小数的下标,第一趟结束后,将第一个数,与暂存的那个最小数进行交换,第一个数就是最小(或最大的数);
下标移到第二位,第二个数跟后面的所有数相比,一趟下来,确定第二小(或第二大)的数;
重复以上步骤,直到指针移到倒数第二位,确定倒数第二小(或倒数第二大)的数,那么最后一位也就确定了,排序完成。
算法动态演示:
代码实现:
import java.util.Arrays;
public class MySort {
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
public static void selectSort(int[] arr) {
// 创建一个变量 bound 表示已排序区间和待排序区间的边界,
// [0, bound) 已排序区间,[bound, length) 待排序区间。
int bound = 0;//对于选择排序来说bound必须从0开始!(区别插入排序)
for (; bound < arr.length; bound++) {
// 内层循环要进行打擂台的过程,擂台的位置就是 bound 下标的位置
for (int cur = bound + 1; cur < arr.length; cur++) {
if (arr[cur] < arr[bound]) {
// 如果发现挑战者比擂主小, 就交换两个元素
swap(arr, cur, bound);
}
}
}
}
public static void main(String[] args) {
int[] arr3 = {9,5,2,7,3,6,8};
System.out.println("原数组:"+Arrays.toString(arr3));
selectSort(arr3);
System.out.println("选择排序处理后的数组:"+Arrays.toString(arr3));
}
}
双向选择排序(补充)
基本思想:
直接选择排序的思路就是每趟将一个最小值归位,但是我们不妨想想,如果在一趟中我们既将最小值归位,也将最大值归位,效率是不是更高?
这也就是双向选择排序的思路,每趟同时保存最大和最小下标,设首位为最小,末尾为最大,从两头往中间不断对比交换,一趟就能够将两个数据归位。 理论上,大小为n的数组只要进行n/2次排序即可。
代码实现:
package Homework0323;
public class doubleSelectionSort {
public static void main(String[] args) {
int[] data = new int[] { 46,74,53,14,26,36,86,65,27,34 };
System.out.println("原始数组:");
print(data);
System.out.println("双向选择排序处理过程:");
DoubleSelectSort(data);
System.out.println("双向选择排序处理后的数组:");
print(data);
}
public static void DoubleSelectSort(int[] array) {
int begin = 0;
int end = array.length - 1;
// [begin,end] 表示整个无序区间
while (begin <= end) {
int minIndex = begin;
int maxIndex = end;//无序区间的最大值和最小值
for (int i = begin ; i <= end; ++i) {
if (array[i] < array[minIndex])
minIndex = i;
if (array[i] > array[maxIndex])
maxIndex = i;
}
swap(array, minIndex, begin);
print(array);
//当首位为最大值时,需要单独处理
if (maxIndex == begin) {
maxIndex = minIndex;
}
swap(array, maxIndex, end);
print(array);
begin++;
end--;
}
}
private static void swap(int[] array, int i, int j) {
int t = array[i];
array[i] = array[j];
array[j] = t;
}
public static void print(int[] data) {
for (int j = 0; j < data.length; j++) {
System.out.print(data[j] + "\t");
}
System.out.println();
}
}
2.堆排序
算法描述:
堆排序是利用数据结构二叉树的结构,并同时满足堆的性质,按照大根堆(根节点大于左右孩子结点)或小根堆(根节点小于左右孩子结点)结点的索引进行结点交换。
我们如果从直观上进行理解:如果想进行升序排序,可以见一个小堆,每次取堆顶元素,依次取N次,那么最后就得到了一个升序排列~~但是,这种排序有一个弊端就是,开辟了新的空间,而不再是原地排序,需要增加算法时间空间效率的消耗!
为了达到原地排序的效果,我们就需要换一个思路,建立一个大堆,每次删除堆顶元素(此处的删除就是将堆顶元素和堆的最后一个元素进行交换),此时,堆上面的最大值虽然从堆上被删除了,但是却跑到数组的末尾位置上了~~
接下来,我们从0号未知的元素进行向下调整,使前面的元素重新成为堆,再次把0号元素和堆里的最后一个元素交换,此时相当于堆上的最大值来到了数组上的倒数第二个位置,依次循环~所以说,堆排序实质上是一个优化版本的选择排序!!!
算法动态演示:
代码实现:
import java.util.Arrays;
public class MySort {
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
public static void heapSort(int[] arr) {
// 1. 先建立堆!!
createHeap(arr);
// 2. 需要循环的取出堆顶元素, 和最后一个元素交换并删除之
// 再从 0 位置进行调整
int heapSize = arr.length;
for (int i = 0; i < arr.length; i++) {
// 交换 0 号元素和堆的最后一个元素
swap(arr, 0, heapSize - 1);
// 把最后一个元素从堆上删除
heapSize--;
// 从 0 号位置开始往下进行调整
shiftDown(arr, heapSize, 0);
}
}
public static void createHeap(int[] arr) {
for (int i = (arr.length - 1 - 1) / 2; i >= 0; i--) {
shiftDown(arr, arr.length, i);
}
}
public static void shiftDown(int[] arr, int size, int index) {
int parent = index;
int child = 2 * parent + 1;
while (child < size) {
// 先找出左右子树比较大的~
if (child + 1 < size && arr[child + 1] > arr[child]) {
child = child + 1;
}
// 再去比较 child 和 parent
if (arr[parent] < arr[child]) {
swap(arr, parent, child);
} else {
break;
}
parent = child;
child = 2 * parent + 1;
}
}
public static void main(String[] args) {
int[] arr4 = {9,5,2,7,3,6,8,1,10,4,0};
System.out.println("原数组:"+Arrays.toString(arr4));
heapSort(arr4);
System.out.println("堆排序处理后的数组:"+Arrays.toString(arr4));
}
}
1.冒泡排序
算法描述:
比较相邻元素,如果第一个比第二个大,就交换;对每一对元素比较,这样完成了一组;重复上述步骤,完场剩下的n-1组。
代码实现:
import java.util.Arrays;
public class MySort {
public static void bubbleSort(int[] arr){
//用bound来区分两个区间
for (int bound = 0;bound < arr.length;bound++){
for (int cur = arr.length - 1;cur > bound;cur--){
if (arr[cur -1] > arr[cur]){
swap(arr,cur-1,cur);
}
}
}
}
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
public static void main(String[] args) {
int[] arr5 = {9,5,2,7,3,6,8,1,10,4,0};
System.out.println("原数组:"+Arrays.toString(arr5));
bubbleSort(arr5);
System.out.println("冒泡排序处理后的数组:"+Arrays.toString(arr5));
}
}
2.快速排序(比较重要)
算法描述:
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
import java.util.Arrays;
public class MySort {
public static void quickSort(int[] arr) {
// 创建一个辅助递归的方法.
// 在这个方法的参数中, 明确指定针对[0, length - 1]区间进行递归.
_quickSort(arr, 0, arr.length - 1);
}
public static void _quickSort(int[] arr, int left, int right) {
if (left >= right) {
// 如果当前的区间为空, 或者只有一个元素,都不需要进行任何处理
return;
}
// 现针对当前 [left, right] 区间进行 partition 操作
// partition方法的返回值, 表示整理完当前区间后, 基准值所在的位置.
// 遍历过程中的 left 和 right 的重合位置
int index = partition(arr, left, right);
// 递归的对左侧区间进行快速排序
_quickSort(arr, left, index - 1);
// 递归的对右侧区间进行快速排序
_quickSort(arr, index + 1, right);
}
public static int partition(int[] arr, int left, int right) {
// 选取最右侧元素作为基准值.
int pivot = arr[right];
int l = left;
int r = right;
// 如果 l 和 r 重合, 说明遍历完成
while (l < r) {
// 先从左往右, 找一个比基准值大的数字.
while (l < r && arr[l] <= pivot) {
l++;
}
// 当循环结束的时候, l 就指向了比基准值大的元素
// 再从右往左, 找一个比基准值小的数字
while (l < r && arr[r] >= pivot) {
r--;
}
swap(arr, l, r);
}
// 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
swap(arr, l, right);
// 最终方法返回基准值所在的位置(l 和 r 重合的位置)
return l;
}
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
public static void main(String[] args) {
int[] arr6 = {9,5,2,7,3,6,8,1,10,4,0};
System.out.println("原数组:"+Arrays.toString(arr6));
quickSort(arr6);
System.out.println("快速排序处理后的数组:"+Arrays.toString(arr6));
}
}
性能分析:
注意事项:
1.在选择基准值时,如果选取了最后一个位置作为基准值,那么接下来必须先从左往右,再从右往左进行比较;如果选取了第一个位置作为基准值,那么接下来必须先从右往左,再从左往右进行比较!
优化手段:
1.选择基准值很重要,通常使用几数取中法;
2. 当代处理区间已经比较小的时候,就不继续递归了,直接针对该区间进行插入排序;
3. 当递归深度达到一定深度的时候,并且当前待处理区间还是比较大~还可以使用堆排序!
快速排序(非递归版本)
public static void quickSortByLoop(int[] arr) {
// 1. 先创建一个栈, 这个栈用来保存当前的每一个待处理区间
Stack<Integer> stack = new Stack<>();
// 2. 把根节点入栈, 整个数组对应的区间
stack.push(0);
stack.push(arr.length - 1);
// 3. 循环取栈顶元素
while (!stack.isEmpty()) {
// 取的元素就是当前的待处理区间
// 取的顺序正好和插入的顺序相反
int right = stack.pop();
int left = stack.pop();
if (left >= right) {
// 如果是空区间或者只有一个元素, 不需要排序
continue;
}
// 调用 partition 方法整理当前区间
int index = partition(arr, left, right);
// 右侧区间: [index + 1, right]
stack.push(index + 1);
stack.push(right);
// 左侧区间: [left, index - 1]
stack.push(left);
stack.push(index - 1);
}
}
1.归并排序
算法描述:
归并排序(Merge sort)是建立在归并操作上的一种有效的排序算法,归并排序对序列的元素进行逐层折半分组,然后从最小分组开始比较排序,合并成一个大的分组,逐层进行,最终所有的元素都是有序的。
算法的动态演示:
import java.util.Arrays;
public class MySort {
public static void mergeSort(int[] arr) {
_mergeSort(arr, 0, arr.length);
}
// 辅助递归的方法
public static void _mergeSort(int[] arr, int left, int right) {
if (right - left <= 1) {
// 判定当前区间是不是只有一个元素或者没有元素,如果true,此时不需要进行排序。
return;
}
int mid = (left + right) / 2;
// 先让 [left, mid) 区间变成有序
_mergeSort(arr, left, mid);
// 再让 [mid, right) 区间变成有序
_mergeSort(arr, mid, right);
// 合并两个有序区间
merge(arr, left, mid, right);
}
// 归并排序中的关键操作, 就是归并两个有序数组。使用该 merge 方法完成数组归并的过程!
// 此处两个数组就通过参数的 left, mid, right 描述:
// [left, mid) 左侧数组,[mid, right) 右侧数组
public static void merge(int[] arr, int left, int mid, int right) {
// 进行具体的归并操作时需要创建一个临时的空间用来保存归并的结果
// 临时空间得能保存下带归并的两个数组,所以容量需要为(right - left)
if (left >= right) {
// 空区间就直接忽略~~
return;
}
int[] tmp = new int[right - left];
int tmpIndex = 0; // 这个下标表示当前元素该放到临时空间的哪个位置上.
int cur1 = left;
int cur2 = mid; //这儿要想清楚,cur1和cur2就相当于两个“指针”的起始位置
while (cur1 < mid && cur2 < right) {
// 由于 cur1 是在左侧区间, cur2 是在右侧区间.
// 此时如果发现 cur1 和 cur2 的值相等,
// 就希望左侧区间的 cur1 在最终结果中仍然是在左侧.
// 于是就先把 cur1 对应的元素给先放到结果中.
if (arr[cur1] <= arr[cur2]) { // 此处 最好写成 <= , 目的就是稳定性.
// 把 cur1 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
} else {
// 把 cur2 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
}
//循环结束之后, 需要把剩余的元素也都给拷贝到最终结果里.
//而且下面两个while循环一定是互斥的,这只能有一个成立!
while (cur1 < mid) {
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
}
while (cur2 < right) {
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
// 还需要把 tmp 的结果再放回 arr 数组. (原地排序)
// 把原始数组的 [left, right) 区间替换回排序后的结果
for (int i = 0; i < tmp.length; i++) {
arr[left + i] = tmp[i];
}
}
public static void main(String[] args) {
int[] arr7 = {4,9,5,2,7,3,6,8,1};
System.out.println("原数组:"+Arrays.toString(arr7));
mergeSort(arr7);
System.out.println("归并排序处理后的数组:"+Arrays.toString(arr7));
}
}
public static void mergeSortByLoop(int[] arr) {
// gap 用于限定分组.
// gap 值的含义就是每个待归并数组的长度
int gap = 1;
for (; gap < arr.length; gap *= 2) {
// 当前两个待归并的数组
for (int i = 0; i < arr.length; i += 2*gap) {
// 在这个循环中控制两个相邻的数组进行归并
// [left, mid) 和 [mid, right) 就要进行归并
int left = i;
int mid = i + gap;
if (mid >= arr.length) {
mid = arr.length;
}
int right = i + 2 * gap;
if (right >= arr.length) {
right = arr.length;
}
merge(arr, left, mid, right);
}
}
}