本文涉及到的排序算法,均属于原地排序。
插入排序
整个区间被分为:有序区间 和 无序区间。
每次选择无序区间的第一个元素,在有序区间内选择合适的位置插入。
参考代码如下:
public static void insertSort(int[] arr) {
// [0, bound) 是已排序区间.
// [bound, length) 待排序区间.
int bound = 1;
for (; bound < arr.length; bound++) {
int v = arr[bound];
int cur = bound - 1;
for (; cur >= 0; cur--) {
if (arr[cur] > v) {
arr[cur + 1] = arr[cur];
}else{
break;
}
}
arr[cur + 1] = v;
}
}
稳定性:稳定
插入排序,初始数据越接近有序,时间效率越高。
希尔排序
希尔排序是对插入排序的进一步改进,它针对序列进行分组,针对每一组进行插入排序,随着调整分组的变化(即分组的距离 gap),使得整个数组逼近有序状态。实际上当分组间隔大于1的时候 即(gap>1)的时候都是预排序,当gap=1的时候已经接近有序了,再整体来一次插入排序,就可以达到排序的效果了
代码参考如下:
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
int bound=gap;
for (;bound<arr.length;bound++){
int v=arr[bound];
int cur=bound-gap;
for (;cur>=0;cur-=gap){
if (arr[cur]>v){
arr[cur+gap]=arr[cur];
}else{
break;
}
}
arr[cur+gap]=v;
}
}
稳定性:不稳定
选择排序
每一次都从无序区间选出最大(最小)的一个元素,存放在无序区间的最后(最前),直到全部待排序的数据元素排完。
参考代码如下:
public static void selectSort(int[] arr){
// [0, bound) 已排序区间
// [bound, length) 待排序区间
int bound=0;
for (;bound<arr.length;bound++){
for (int cur=bound+1;cur<arr.length;cur++){
//通过打擂台的方式进行比较,满足条件就交换两个元素
if (arr[cur]<arr[bound]){
swap(arr,cur,bound);
}
}
}
}
public static void swap(int[] arr,int x,int y){
int tmp=arr[x];
arr[x]=arr[y];
arr[y]=tmp;
}
稳定性:不稳定
冒泡排序
在无序区间,通过相邻数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序
参考代码如下:
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 heapSort(int[] arr){
//先建立堆
createHeap(arr);
int heapSize=arr.length;
// 需要循环的取出堆顶元素, 和最后一个元素交换并删除之
// 再从 0 位置进行调整
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;
}
if (arr[parent] <arr[child]){
swap(arr,parent,child);
}else{
break;
}
parent=child;
child=2 *parent+1;
}
}
public static void swap(int[] arr, int x, int y) {
int tmp = arr[x];
arr[x] = arr[y];
arr[y] = tmp;
}
稳定性:不稳定
快速排序
1.
a.
从待排序区间选择一个数,作为基准值
(pivot)
;
b. Partition:
遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
c.
采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度
== 1
,代表已经有序,或者小区间的长度 == 0
,代表没有数据。
参考代码如下:
public static void quickSort(int[] arr) {
// 创建一个辅助递归的方法.
// 在这个方法的参数中, 明确指定针对哪个区间进行递归.
_quickSort(arr, 0, arr.length - 1);
}
public static void _quickSort(int[] arr, int left, int right) {
if (left >= right) {
return;
}
// 现针对当前 [left, right] 区间进行 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 v = arr[right];
int l = left;
int r = right;
// 如果 l 和 r 重合, 说明遍历完成
while (l < r) {
// 先从左往右, 找一个比基准值大的数字.
while (l < r && arr[l] <= v) {
l++;
}
// 当循环结束的时候, l 就指向了比基准值大的元素
// 再从右往左, 找一个比基准值小的数字
while (l < r && arr[r] >= v) {
r--;
}
swap(arr, l, r);
}
// 当 l 和 r 重合的时候, 就把重合位置的元素和基准值位置进行交换
swap(arr, l, right);
// 最终方法返回基准值所在的位置(l 和 r 重合的位置)
return l;
}
2.或者采用栈的方式实现:
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 的时候就一定是有序的。
参考代码如下:
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) {
// 判定当前区间是不是只有一个元素或者没有元素
// 此时不需要进行排序
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;
while (cur1 < mid && cur2 < right) {
if (arr[cur1] <= arr[cur2]) {
// 把 cur1 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur1];
tmpIndex++;
cur1++;
} else {
// 把 cur2 对应的元素插入到临时空间中
tmp[tmpIndex] = arr[cur2];
tmpIndex++;
cur2++;
}
}
// 循环结束之后, 需要把剩余的元素也都给拷贝到最终结果里.
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];
}
}
稳定性:稳定