插入排序
直接插入排序
基本思想:将未排序的数据元素,在已经排好的有序序列中从后向前扫描,找到对应位置然后插入。
在扫描的过程中,需要反复把已经排序的元素向后挪动,为准备新插入的元素提供空间当插入第 i(i>=1) 个元素时,前面的 array[0],array[1],…,array[i-1] 已经排好序,此时用 array[i] 的排序码与 array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将 array[i] 插入,原来位置上的元素顺序后移。
过程演示:
代码:
public static void insertSort(int[] arr) {
//第一个元素有序,从第二个数开始比较
for (int i = 1; i < arr.length; i++) {
//tmp 记录 i 下标的值
int tmp = arr[i];
//每次比较循环后,j 回到 i的前一个位置
int j = i - 1;
//j 小于0时就不用比较
for (; j >= 0; j--) {
if (tmp < arr[j]){
//满足条件就让 j的前一个位置 等于 j的位置
arr[j+1] = arr[j];
}else {
//不满足说明前面都是有序的
break;
}
}
//j减了1,所以要让j+1的位置等于tmp
arr[j+1] = tmp;
}
}
希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序数据分成多个组,并对每一组内的记录进行插入排序。然后重复上述分组和排序的工作。当 gap=1 时,所有记录在一组内排好序。 例如:
第一趟:gap = 数组长度除以 2,等于4,分为 4 组:
第二趟,分为两组:
第三趟:
代码:
public static void shellSort(int[] arr) {
int gap = arr.length;
while (gap > 1) {
//循环进来 ,gap 就除以2,最后变成1
gap /= 2;
shell(arr, gap);
}
}
public static void shell(int[] arr, int gap) {
//i从gap开始,每次只需加1,可以比较完一组的两个后比较另一组的两个
for (int i = gap; i < arr.length; i++) {
//tmp 记录 i 下标的值
int tmp = arr[i];
//j 为 i的前gap位置
int j = i - gap;
//j 小于0时就不用比较,每次跳转到前gap个位置
for (; j >= 0; j -= gap) {
if (tmp < arr[j]) {
//满足条件就让 j的前gap个位置 等于 j的位置
arr[j + gap] = arr[j];
} else {
//不满足说明前面都是有序的
break;
}
}
arr[j + gap] = tmp;
}
}
为什么要像上面一样分组,而不能把相近的两个分为一组?
选择排序
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
直接选择排序
步骤:
- 在元素集合 array[i]–array[n-1] 中选择关键码最大(小)的数据元素。
- 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换。
- 在剩余的array[i]–array[n-2](array[i+1]–array[n-1])集合中,重复上述步骤,直到集合剩余1个元素。
代码:
public static void selectSort(int[] arr) {
for (int i = 0; i < arr.length; i++) {
//假设最小元素下标
int minIndex = i;
for (int j = i + 1; j < arr.length; j++) {
if (arr[minIndex] > arr[j]) {
//找到比minIndex下标还小的元素
minIndex = j;
}
}
swap(arr, i, minIndex);
}
}
堆排序
堆排序(Heapsort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种,通过堆来进行选择数据。注意排升序要建大堆,排降序建小堆。 不了解堆的可以先看看这篇文章(数据结构----堆)
使用排大堆的方法:
堆排序步骤:
- 建造一个大根堆(升序)或小根堆(降序)。
- 第一个元素和最后一个未确定位置的元素交换。
- 对第一个元素向下调整。
- 重复以上步骤,直到所有元素有序。
现在的问题就是怎么建立一个大根堆?
堆排序代码:
public static void heapSort(int[] arr) {
//建立大根堆
createBigHeap(arr);
//指向最后一个值
int end = arr.length - 1;
while (end > 0) {
//先交换
swap(arr, 0, end);
//交换后再向下调整
shutDown(arr, 0, end);
//end减减,保证确定好位置的元素不会被调整成大根堆
end--;
}
}
//建立大根堆
public static void createBigHeap(int[] arr) {
//从最后一个非叶子结点开始往上使用向下调整
for (int parent = (arr.length - 1 - 1) / 2; parent >= 0; parent--) {
shutDown(arr, parent, arr.length);
}
}
//向下调整
public static void shutDown(int[] arr, int parent, int len) {
//左孩子的下标
int child = parent * 2 + 1;
//只要左孩子结点不超过数组大小
while (child < len) {
//如果有右孩子并且右孩子的值大于左孩子 让child+1,
//保证指向的是左右孩子中最大的结点
if (child + 1 < len && arr[child] < arr[child + 1]) {
child++;
}
//如果大于父节点就交换
if (arr[child] > arr[parent]) {
swap(arr, child, parent);
//元素交换后,下面的结点可能会被影响,
//继续往下走,重复上面的比较 交换
parent = child;
child = parent * 2 + 1;
} else {
//不大于就退出,因为是从最后一个非叶子结点开始调整的,
//所以后面也是有序的
break;
}
}
}
交换排序
基本思想:根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。
冒泡排序
冒泡排序(Bubble Sort)是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
过程演示:
而在第三趟比较中,元素顺序已经有序,后面就可以不比较了,所以我们可以在每趟比较前,设置一个标记,只要有交换就更改标记,一趟下来没有交换说明数组就是有序的,直接退出。
代码:
public static void bubbleSort(int[] arr) {
//i从0开始,比较数组长度减一趟
for (int i = 0; i < arr.length - 1; i++) {
//假设都是有序的
boolean flag = true;
//每趟比较的次数
for (int j = 0; j < arr.length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
//前一个数比后一个数大,不是有序,更改标记并交换
flag = false;
swap(arr, j, j + 1);
}
}
//一趟下来,看标记是否有序
if (flag) {
return;
}
}
}
public static void bubbleSort2(int[] arr) {
//i从1开始,比较数组长度减一趟
for (int i = 1; i <= arr.length - 1; i++) {
boolean flag = true;
for (int j = 0; j < arr.length - i; j++) {
if (arr[j] > arr[j + 1]) {
flag = false;
swap(arr, j, j + 1);
}
}
if (flag) {
return;
}
}
}
快速排序
快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,基本思想:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。
快速排序的关键就在于把序列分为两部分,按照基准值将区间划分为左右两半部分的常见方式有:
挖坑法
先将第一个数据存放在临时变量 key 中,形成一个坑位,从右往左找一个比 key 小的元素填到这个坑里,新的坑就变成刚刚比 key 小的所在的位置,又从左往右找一个比 key 大的元素填到新的坑,重复上面步骤将数组分隔成两部分。
过程演示:
代码:
public static void quickSort(int[] arr) {
quick(arr, 0, arr.length - 1);
}
private static void quick(int[] arr, int left, int right) {
//相遇的地方可能在第一个,也可能在最后面
if (left >= right) {
return;
}
//获取相遇的地方
int pivot = partition(arr, left, right);
//把相遇地方的左右两边的元素都排在相应位置
quick(arr, left, pivot - 1);
quick(arr, pivot + 1, right);
}
//挖坑法
private static int partition(int[] arr, int left, int right) {
//以左边确定key
int key = arr[left];
while (left < right) {
//找到比key小
while (left < right && arr[right] >= key) {
right--;
}
arr[left] = arr[right];
//找到比key大
while (left < right && arr[left] <= key) {
left++;
}
arr[right] = arr[left];
}
//left 和 right相遇
arr[left] = key;
//返回他们相遇的地方
return left;
}
Hoare版
记录下初始位置,right 从后往前找到比 key 小的元素,停下来,left 从前往后找到比 key 大的元素,停下来,left 和 right 所指向的元素交换,重复上面步骤,直到 left 和 right 相遇,相遇的地方和初始位置交换,返回交换地方,此时就将区间分为两部分。
过程演示:
代码:
//Hoare法
private static int partition1(int[] arr, int left, int right) {
//以左边比较
int key = arr[left];
//记录下开始位置
int begin = left;
while (left < right) {
//找到比key小
while (left < right && arr[right] >= key) {
right--;
}
//找到比key大
while (left < right && arr[left] <= key) {
left++;
}
swap(arr, left, right);
}
//交换相遇地方和开始地方的元素
swap(arr, left, begin);
//返回他们相遇的地方
return left;
}
快速排序—非递归
代码:
public static void quickSort2(int[] arr) {
Deque<Integer> stack = new LinkedList<>();
int left = 0;
int right = arr.length - 1;
int pivot = partition(arr, left, right);
if (pivot > left + 1) {
//左边不止一个元素
stack.push(left);
stack.push(pivot - 1);
}
if (pivot < right - 1) {
//右边不止一个元素
stack.push(pivot + 1);
stack.push(right);
}
while (!stack.isEmpty()) {
//left先进栈,出栈就先赋值给right
right = stack.pop();
left = stack.pop();
//对出栈的left 和right确定基准值的位置
pivot = partition(arr, left, right);
if (pivot > left + 1) {
stack.push(left);
stack.push(pivot - 1);
}
if (pivot < right - 1) {
stack.push(pivot + 1);
stack.push(right);
}
}
}
归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 也就是一分二,二分四,一直分,直到分成单个的元素,然后按他们的大小开始合并,直到所有数据有序。
代码:
//归并排序
public static void mergeSort(int[] arr) {
mergeSortFunc(arr, 0, arr.length - 1);
}
//分割代码
private static void mergeSortFunc(int[] arr, int left, int right) {
if (right <= left) {
return;
}
int mid = (left + right) / 2;
mergeSortFunc(arr, left, mid);
mergeSortFunc(arr, mid + 1, right);
merge(arr, left, right, mid);
}
//合并代码
private static void merge(int[] arr, int left, int right, int mid) {
int left1 = left;
int right1 = mid;
int left2 = mid + 1;
int right2 = right;
int[] tmp = new int[right - left + 1];
int k = 0;
while (left1 <= right1 && left2 <= right2) {
if (arr[left1] <= arr[left2]) {
tmp[k++] = arr[left1++];
} else {
tmp[k++] = arr[left2++];
}
}
//第二组数据可能一直比第一组的小,所以需要将第一组的添加进来
while (left1 <= right1) {
tmp[k++] = arr[left1++];
}
while (left2 <= right2) {
tmp[k++] = arr[left2++];
}
//拷贝到原数组中
for (int i = 0; i < tmp.length; i++) {
//加上left是因为第二组下标是从left开始
arr[i + left] = tmp[i];
}
}
各排序比较
排序 | 时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
插入排序 | O(n2) | O(1) | 稳定 |
希尔排序 | O(n1.3) | O(1) | 不稳定 |
选择排序 | O(n2) | O(1) | 不稳定 |
堆排序 | O(N*logN) | O(1) | 不稳定 |
冒泡排序 | O(n2) | O(1) | 稳定 |
快速排序 | O(N*logN) | O(logN) | 不稳定 |
归并排序 | O(N*logN) | O(N) | 稳定 |
稳定性是指两个相同的数在排序后和排序前是否改变它们相对的顺序。例如: