七大排序
- 交换排序
- 冒泡排序
- 快速排序
- 插入排序
- 直接插入排序
- 希尔排序
- 选择排序
- 直接选择排序
- 堆排序
- 归并排序
1、冒泡排序
每次比较相邻的两个元素,将较大的元素后移,每当将数组的数比较一次,最大的数都会出现在数组的最后,再对除去最后面数的前面的数进行重复操作,操作arr.length-1次即可
代码如下:
private static void bubbleSort(int[] arr){
for (int i = arr.length - 1; i > 0; i--){
for (int j = 0; j < i; j++){
// 相等不交换,保证稳定性
if (arr[j] > arr[j+1]){
swap(arr, j, j+1);
}
}
}
}
private static void swap(int[] arr, int a, int b){
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
2、快速排序
随机选择一个基准(一般选择第一个数),再设置两个辅助“指针”:i,j,i是第一个元素的index,j为最后一个元素的index。首先j开始向前移动,直到找到第一个比基准小的数index为止,然后i向后移动,直到找到第一个比基准大的数的index为止,交换i,j位置的元素;再重复上述操作,直到i>=j为止,此时将基准的值与i位置的值交换。则左边的都是比基准小的值,右边都是比基准大的值。再对左右两边进行同样的操作。
代码如下:
/**
*
* 快速排序:选择一个元素作为基准(一般选择第一个元素),begin指向开头,end指向末尾
* end负责找出比基准小的第一个元素,找出后,begin找出第一个比基准大的元素
* 交换两个元素,以此类推,直到begin和end碰到,交换基准与该位置的元素;
* 此时,该位置的左边是比基准小的元素,右边是比基准大的元素;以该位置作为
* 分隔点,对左边和右边进行相同的操作;当begin大于等于end的时候,跳出递归
*
* @param arr 目标数组
* @param begin 开始位置
* @param end 结束位置
*/
private static void quickSort(int[] arr, int begin, int end){
if (begin >= end){
return;
}
int standard = arr[begin];
int i = begin;
int j = end;
while (i < j){
while (i < j && arr[j] >= standard){
j--;
}
while (i < j && arr[i] <= standard){
i++;
}
if (i < j){
swap(arr, i, j);
}
}
swap(arr, begin, i);
quickSort(arr, begin, i-1);
qucikSort(arr, i+1, end);
}
3、直接插入排序
从无序序列向有序序列里面插入元素,一开始先选择数组的第一个元素作为有序序列,从第二个元素往后是无序序列,从无序序列逐个拿出元素插入到有序序列里面就是直接插入排序。
有序区间[0,i);无序区间[i,arr.length); i:1 -- > arr.length-1
代码如下:
private static void insertSort(int[] arr){
// 有序区间是[0,i)
// 无序区间是[i,arr,length)
// 初始化有序区间就是[0,1),无序区间是[1,arr.length)
for (int i = 1; i <= arr.length - 1; i++){
int v = arr[i]; // 取无序区间的第一个数,分别和有序区间的数进行倒序比较
int j = i - 1; // 有序区间的最后一个数的下标
// 不写 arr[j] == v 是保证排序的稳定性
while (j >=0 && arr[j] > v){
// 如果j有效且有序区间的最后一个值大于无序的第一个值,将有序的最后一个值后移
arr[j+1] = arr[j];
// 有序index前移,无序的最后一个值和有序的倒数第二个值进行比较
j--;
}
// 如果发生了移动,就将无序的第一个值放在恰当的地方
if (j+1 != i){
arr[j+1] = v;
}
}
}
4、希尔排序
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成若干个组,所
有距离为gap的记录分在同一组内,并对每一组内的记录进行排序。然后取下一个gap,重复上述分组和排序的工作。当到达gap=1
时,所有记录在统一组内排好序。一般首先取gap初始值为gap = arr.length/2,然后 gap /= 2,直到gap=1停止。
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很
快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
代码如下:
/**
* 希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,
* 把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,
* 并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当到达gap=1
* 时,所有记录在统一组内排好序。
* 1. 希尔排序是对直接插入排序的优化。
* 2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,
* 数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。
* @param arr 目标数组
*/
private static void shellSort(int[] arr){
for (int gap = arr.length / 2; gap >= 1; gap /= 2){ // gap是间隔的大小
for (int i = 1; i < arr.length; i++){ // 从下标为1的元素开始找前面距离它为gap的元素
int v = arr[i]; // 令v等于当前位置的元素
int j = i - gap; // j为距离当前元素为gap的前面的第一个元素的index
for (; j >= 0 && arr[j] > v; j -= gap){ // 当j>=0的时候,该index有效
// 在index有效的前提下,若前面的元素大于当前的元素
arr[j+gap] = arr[j]; // 将前面的元素放到当前位置
// 然后 j=j-gap,再找更前面的距离两个,三个,四个gap的元素的index。
// 直到index的值小于0或者前面的小于后面的值,跳出循环
// 因为gap的起始值为arr.length / 2,所以最开始一定只有两个元素比较
// 所以在比它小的值开始,前面的值一定是小于它的,就是直接插入排序,
// 前面的一定是有序数组
}
if (j+gap != i){// 如果前面第一个值就比该位置的值小,那就不用动,此时 j+gap = i
// 只有当发生一次替换的时候,才需要将当前位置的值放到前面的位置
arr[j+gap] = v;
}
}
}
}
5、直接选择排序
每次从数组里面找出最大(小)的值,用minIndex或者maxIndex记录该值的下标,将该值放在数组的末尾(开头)。
代码如下:
//每次选出最大的数往后放
private static void selectMaxSort(int[] arr){
for (int i = arr.length-1; i > 0; i--){
int maxIndex = i;
for (int j = i - 1; j >= 0; j--){
if (arr[maxIndex] < arr[j]){
maxIndex = j;
}
}
if (maxIndex != i){
swap(arr, maxIndex, i);
}
}
}
// 每次找出最小的数往前放
private static void selectMinSort(int[] arr){
for (int i = 0; i < arr.length - 1; i++){
int minIndex = i;
for (int j = i + 1; j <= arr.length - 1; j++){
if (arr[minIndex] > arr[j]){
minIndex = j;
}
}
if (minIndex != i){
swap(arr, minIndex, i);
}
}
}
6、堆排序
创建一个大顶堆(升序)或者小顶堆(降序),每次调整大顶堆都会使最大的元素的出现在堆顶,也即是arr[0],然将该元素和数组最后的结点交换,也就是数组的尾部,再对前面的结点再次进行调整。
父节点和孩子结点的关系是:父节点 i ,左孩子 2 * i + 1,右孩子 2 * i + 2。
代码如下:
// 向下调整大顶堆
private static void heapify(int[] arr, int root, int len){
int parent = root; // 初始化时,父节点从最后一个非叶子结点开始,初始值为:(arr.length-2)/2
int left = 2 * parent + 1; // 左孩子
while (left < len){ // 如果存在做左孩子
if (left + 1 < len && arr[left + 1] > arr[left]){// 如果存在右孩子且右孩子大于左孩子
left++; // left自加一,为右孩子的index,就是left是两个孩子的最大值的下标
}
if (arr[parent] < arr[left]){ // 如果孩子的最大值大于父节点
swap(arr, parent, left); // 交换父节点与孩子结点
parent = left; // 再令父节点为交换后的孩子结点
left = 2 * parent + 1;//目的是为了对交换后的进行再排序,以免交换破坏下面堆的结构
}else{ // 如果孩子的最大值小于父节点,直接跳出循环,不需要调整该节点
break;
}
}
}
// 初始化生成一个大顶堆
private static void createMaxHeap(int[] arr){
int len = arr.length;
for (int i = (len - 2) / 2; i >= 0 ; i--){ // 一定要倒序生成大顶堆
heapify(arr, i, len);
}
}
// 堆排序
private static void heapSort(int[] arr){
int end = arr.length - 1; // 记录数组尾下标
createMaxHeap(arr); // 初始化大顶堆
while (end > 0){ // 当end为1的时候,也就是交换了0,1的值,
// 这个时候的数组就是有序数组,跳出调整,
//也即是长度为len的数组排序的过程中需要创建len-1个大顶堆
//即找出len-1个最大数
swap(arr, 0, end); // 交换大顶堆顶部的值和数组末尾的值,即将最大值后置
heapify(arr, 0, end);// 将大数后置的大顶堆再次进行调整,就相当从root=0发生从
//root=0开始向下调整即可,不需要像初始化大顶堆那样倒序调整
// 第三个参数是数组的长度,因为最后一个数已经是最大的值,
// 不需要再参与排序,而且end的初始值就是len-1,
// 所以只需要操作原数组的len-1个值,即原数组的前end个元素即可。
end--; // 调整后end自减1,为了使下次操作的元素个数减1
}
}
7、归并排序
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
将数组划分为最小的块,起始有序,将融合两个有序的块,最后使整个数组有序。属于外排序。
代码如下:
// 融合两个有序的数组,大数组为[low,high)。
// 譬如{1,3,5,7,2,4,6},low = 0, mid = 4, high = 7,融合[0, 4) [4,7)
/**
*
* 合并两个有序的列表 [[low,mid),[mid,high)] 闭开区间
*
* @param arr 目标数组为从mid位置开始,前后分别为有序的两个数组合成的一个数组
* @param low 从那一位开始进行比较融合,下标从0开始
* @param mid 前后两个列表的分界点
* @param high 后者数组的高位,是你想合并到的数的下标+1的值,因为是闭开区间
*/
private static void merge(int[] arr, int low, int mid, int high){
int i = low; // 从哪里开始排序
int j = mid; // 两个数组的分界点
int k = 0; // 给extra添加元素的时候用到
int[] length = high - low; // 数组的长度
int extra = new int[length]; // 新建一个拷贝用的数组
while (i < mid && j < high){ // i和j分别为两个数组的“指针”
// 加入等于,保证稳定性
if (arr[i] <= arr[j]){ // 前面的小
extra[k++] = arr[i++]; // extra保存前面的,i后移,k后移
}else{ // 后面的小
extra[k++] = arr[j++]; // extra保存后面的,j后移,k后移
}
}
while (i < mid){ // 前面的数组长
extra[k++] = arr[i++]; // 直接将剩下的数放到extra里面
}
while (j < high){ // 后面的数组长
extra[k++] = arr[j++]; // 直接将剩下的数放到extra里面
}
for (int index = 0; index < length; index++){
// 需要搬回原位,从low开始
arr[low + index] = extra[index];
}
}
// 划分待排序的区间[low, high)
/**
* 如果区间只剩下一个元素,那么会直接return,而不执行merge();
* 当区间剩下两个元素的时候,再此划分会只剩一个元素,两个mSI都会跳出,然后执行meger()
* 方法,执行完成两个的时候,会跳出当前的递归,执行三个或者四个元素的方法,执行merger()
* 方法,直到将整个数组按照顺序merge
*
* @param arr 目标数组
* @param low 起始位置
* @param high 结束位置
*/
private static void mergeSortInternal(int[] arr, int low, int high){
//System.out.println("进入区间方法");
if (low == high-1){ // 当区间里面的元素划分到只剩下一个元素的时候,跳出递归
//System.out.println("跳出递归");
return; // 区间剩下一个元素会在此处跳出
}
int mid = (low + high) / 2; // low + high 才能让后面序列的起始值从数组的头开始算起
mergeSortInternal(arr, low, mid); // 递归前半区间
mergeSortInternal(arr, mid, high); // 递归后半区间
//System.out.println("区间是:[" + low + ", " + high + ")");
merge(arr, low, mid, high); // 区间剩下俩个以上元素,会执行完成merge()之后跳出
//System.out.println("执行完毕merge");
}
// 二路归并算法
private static void mergeSort(int[] arr){
mergeSortInternal(arr, 0, arr.length);
}
分析划分待排序区间递归的过程
与方法的实际输出顺序相同:
非递归的版本:
public static void mergeSort(int[] array) {
for (int i = 1; i < array.length; i = i * 2) {
for (int j = 0; j < array.length; j = j + 2 * i) {
int low = j;
int mid = j + i;
if (mid >= array.length) {
continue;
}
int high = mid + i;
if (high > array.length) {
high = array.length;
}
merge(array, low, mid, high);
}
}
}