目录
1. 常见排序算法分类
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
- 内部排序:数据元素全部放在内存中的排序。 (本篇博客讲的都属于内部排序~)
- 外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
- 稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,若两个相同的数排完序之后还能保持原有的顺序,则称之为稳定的排序,否则是不稳定的排序。
下图是总结的排序算法简单的一个思维导图,以及它们各自的算法思想!
2. 排序算法的实现
2.1 直接插入排序
算法思想:默认第一个数字是有序的,从第二个数字开始依次和前面的数字进行比较,如果发现有比该数大的数字,则将大数向后移动,直到发现比该数小的数字或者数组下标越界的时候,再将该数插入到合适位置。
public static void insertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int temp = array[i];
int j = 0;
for (j = i-1; j >= 0; j--) {
if (array[j] > temp) {
array[j + 1] = array[j];
}else {
break;
}
}
array[j + 1] = temp;
}
}
- 直接插入排序特性总结:
① 元素集合规模越小或者越接近有序时,直接插入排序算法的时间效率越高!!!
② 时间复杂度:O(n^2)
,最好情况下(数组本来就是有序的情况下)是O(n)
③ 空间复杂度:O(1)
④ 稳定性:稳定,没有发生跳跃式交换
2.2 希尔排序
算法思想:又叫缩小增量排序,首先它把较大的数据集合分割成若干个小组(逻辑上分组),然后对每一个小组组内分别进行直接插入排序,此时直接插入排序所作用的数据量比较小(每一个小组),插入的效率比较高。如此说来,那么希尔排序就可以说是对直接插入排序的优化!
public static void shellSort(int[] array) {
int[] drr = {5, 3, 1};//假设增量序列如此
//不管是几组,算法思想都是一样的
for (int i = 0; i < drr.length; i++) {
shell(array, drr[i]);
}
}
public static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int temp = array[i];
int j = 0;
for (j = i - gap; j >= 0; j -= gap) {
if (array[j] > temp) {
array[j + gap] = array[j];
} else {
break;
}
}
array[j+gap] = temp;
}
}
- 希尔排序特性总结:
① 希尔排序是对直接插入排序的优化
② 当gap > 1
时都是预排序,目的是让数组更接近于有序。当gap == 1
时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果
③ 时间复杂度:O(n^1.3) ~ O(n^1.5)
,(时间复杂度不好计算,需要进行推导)
④ 空间复杂度:O(1)
⑤ 稳定性:不稳定
2.3 选择排序
算法思想:i 等于待排序序列的开始,j 等于待排序序列的后一个元素,j 往后找比 i 小的数字,两者进行交换,走完一次之后就可以找到最小的元素。
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
for (int j = i + 1; j < array.length; j++) {
if (array[i] > array[j]) {
int temp = array[i];
array[i] = array[j];
array[j] = array[i];
}
}
}
}
- 选择排序特性总结:
① 直接选择排序思考非常好理解,但是效率不是很好,实际中很少使用
② 时间复杂度:O(n^2)
③ 空间复杂度:O(1)
④ 稳定性:不稳定
2.4 堆排序
堆排序请移步这里哦~
2.5 冒泡排序
算法思想:它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果他们的顺序(如从大到小、首字母从A到Z)错误就把他们交换过来,越大的元素会经由交换慢慢“浮”到数列的顶端。
- 根据以上规律,可得假设待排序序列中有
n
个元素,趟数用i
表示,那么一共会进行n-1
趟排序,每趟比较n-i
次。(注意,下述代码趟数是从 0 开始计算的,所以每趟会比较 n-i-1 次!)
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {//控制趟数
for (int j = 0; j < array.length - i - 1; j++) {//每趟比的次数
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
}
- 冒泡排序特性总结:
① 时间复杂度:O(n^2)
② 空间复杂度:O(1)
③ 稳定性:稳定 - 冒泡排序的优化:
如果排了几趟之后,发现已经是有序的了,那么后面就不再需要比较了,就像上述例子一样后面三趟都是不需要的,这样一来,时间复杂度最好情况下就是O(n)
了。 - 解决方法:可以考虑加个变量,一旦发现序列已经有序了,就不再进行比较了。
public static void bubbleSort(int[] array) {
boolean swap = false;
for (int i = 0; i < array.length - 1; i++) {//控制趟数
swap = false; //因为不知道从哪一趟是不需要交换的,先置为false
for (int j = 0; j < array.length - i - 1; j++) {//每趟比的次数
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
swap = true; //如果发现交换把swap置为true
}
}
if (!swap) {
break; //如果一趟走完没有发生交换,则已经有序了
}
}
}
2.6 快速排序
算法思想:使用分治的思想,通过一趟排序将待排序序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小,之后的分别对这两部分记录继续进行排序,以达到整个序列有序的目的。
2.6.1 递归法
public static int partion(int[] array, int low, int high) {
int temp = array[low];
while (low < high) {
while (low < high && array[high] > temp) {
high--;//high有可能会越界
}
array[low] = array[high];
while (low < high && array[low] < temp) {
low++;
}
array[high] = array[low];
}
array[low] = temp;
return low;
}
public static void quick(int[] array, int start, int end) {
//递归了多少次,par就保存了多少次
int par = partion(array, start, end);//一趟快排完毕
//递归左边,首先得保证左边有两个数据元素以上
if (par > start + 1) {
quick(array, start, par-1);
}
//递归右边
if (par < end - 1) {
quick(array, par + 1, end);
}
}
public static void quickSort1(int[] array) {
quick(array, 0, array.length - 1);
}
- 快速排序的优化:
- 根据以上分析不难发现,在待排序序列可以被分割成均匀的两部分时快排效率最高,但是这显然不太符合实际,万一本来就是有序的呢?那岂不是浪费太多时间了吗?
那么问题就在于如何把待排序序列均匀的分割成两部分?
① 优化1:三数取中法array[mid] <= array[low] <= array[high]
② 优化2:在排序过程中,待排序序列会逐渐趋于有序。如果某一段序列已经有序,再对这一部分序列进行快排,就会变成冒泡排序。所以到达某一个区间后用直接插入排序。
public static int partion(int[] array, int low, int high) {
int temp = array[low];
while (low < high) {
while (low < high && array[high] > temp) {
high--;//high有可能会越界
}
array[low] = array[high];
while (low < high && array[low] < temp) {
low++;
}
array[high] = array[low];
}
array[low] = temp;
return low;
}
public static void insertSort(int[] array, int start, int end) {
for (int i = start + 1; i <= end; i++) {
int temp = array[i];
int j = 0;
for (j = i-1; j >= start; j--) {
if (array[j] > temp) {
array[j + 1] = array[j];
}else {
break;
}
}
array[j + 1] = temp;
}
}
public static void quick(int[] array, int start, int end) {
//假设对start到end这部分序列有16个元素,已经有序则进行直接插入排序
if (end - start + 1 <= 16) {
insertSort(array, start, end);
return;
//如果这部分序列用直接插入排序完成了,那么剩下的函数不需要再进行了
}
//每次找基准之前先三数取中一下
medianOfThree(array, start, end);
//递归了多少次,par就保存了多少次
int par = partion(array, start, end);//一趟快排完毕
//递归左边,首先得保证左边有两个数据元素以上
if (par > start + 1) {
quick(array, start, par - 1);
}
//递归右边
if (par < end - 1) {
quick(array, par + 1, end);
}
}
public static void quickSort1(int[] array) {
quick(array, 0, array.length - 1);
}
//三数取中法法找基准
public static void medianOfThree(int[] array, int low, int high) {
int mid = (low + high) / 2;
if (array[mid] > array[low]) {
swap(array, low, mid);
}
if (array[low] > array[high]) {
swap(array, low, high);
}
}
public static void swap(int[] array, int low, int high) {
int temp = array[low];
array[low] = array[high];
array[high] = temp;
}
- 快速排序特性总结:
① 时间复杂度:最好:O(n*logn)
最坏:O(n^2)
② 空间复杂度:最好:O(logn)
最坏:O(n) -> 123456789
③ 稳定性:不稳定
2.6.2 非递归法
public static int partion(int[] array, int low, int high) {
int temp = array[low];
while (low < high) {
while (low < high && array[high] > temp) {
high--;//high有可能会越界
}
array[low] = array[high];
while (low < high && array[low] < temp) {
low++;
}
array[high] = array[low];
}
array[low] = temp;
return low;
}
public static void quickSort2(int[] array) {
int low = 0;
int high = array.length-1;
Stack<Integer> stack = new Stack<>();
int par = partion(array, low, high);
if (par > low + 1) {
stack.push(low);
stack.push(par - 1);
}
if (par < high - 1) {
stack.push(par + 1);
stack.push(high);
}
while (!stack.isEmpty()) {
high = stack.pop();
low = stack.pop();
par = partion(array, low, high);
if (par > low+1) {
stack.push(low);
stack.push(par-1);
}
if (par < high-1) {
stack.push(par+1);
stack.push(high);
}
}
}
2.7 归并排序
算法思想:建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
2.7.1 递归法
public static void merge1(int[] array, int start, int mid, int end, int[] tempArray) {
//不断的创建销毁对象,效率太低,所以直接将tempArray作为参数传进来
//int[] tempArray = new int[array.length];
int tempIndex = start;//设置为start,防止把原来数据覆盖掉
int i = start;//把start保存下来是为了接下来拷贝
int s2 = mid + 1;
//当有两个归并段并且两个归并段都有数据的时候
while (start <= mid && s2 <= end) {
if (array[start] < array[s2]) {
tempArray[tempIndex++] = array[start++];
}else {
tempArray[tempIndex++] = array[s2++];
}
}
//第一个归并段有数据,说明第二个归并段已经走完
while (start <= end) {
tempArray[tempIndex++] = array[start++];
}
//第二个归并段有数据,说明第一个归并段已经走完
while (s2 <= end) {
tempArray[tempIndex++] = array[s2++];
}
//把排好序的数据全部从tempArray拷贝到Array中
while (i <= end) {
array[i] = tempArray[i++];
}
}
public static void mergeSort1(int[] array, int start, int end, int[] tempArray) {
if (start >= end) {
return;
}
int mid = (start+end)/2;
//递归左边
mergeSort1(array, start, mid, tempArray);
//递归右边
mergeSort1(array, mid+1, end, tempArray);
//单个有序之后进行二路合并
merge1(array, start, mid, end, tempArray);
}
- 归并排序特性总结:
① 时间复杂度:O(n*logn)
② 空间复杂度:O(n)
③ 稳定性:稳定
2.7.2 非递归法
public static void merge2(int[] array, int gap) {//gap是每组里面的数据个数
int[] tempArray = new int[array.length];
int i = 0;//表示的是tempArray的下标
int start1 = 0;
int end1 = start1 + gap - 1;
int start2 = end1 + 1;
int end2 = start2 + gap - 1 <= array.length - 1 ? start2 + gap - 1 : array.length - 1; //end2可能会数组越界
//保证有两个归并段
//比较的时候肯定有一个归并段先没有数据
while (start2 < array.length) {
//两个归并段都有数据
while (start1 <= end1 && start2 <= end2) {
if (array[start1] < array[start2]) {
tempArray[i++] = array[start1++];
}else {
tempArray[i++] = array[start2++];
}
}
//第一个归并段有数据
while (start1 <= end1) {
tempArray[i++] = array[start1++];
}
//第二个归并段有数据
while (start2 <= end2) {
tempArray[i++] = array[start2++];
}
//证明一次二路归并已经完成
start1 = end2 + 1;
end1 = start1 + gap - 1;
start2 = end1 + 1;
end2 = start2+gap-1 <= array.length-1 ? start2+gap-1 : array.length-1;
}
//只有一个归并段
while (start1 < array.length) {
tempArray[i++] = array[start1++];
}
//把数据从tempArray拷贝到原始数组
for (int j = 0; j < tempArray.length; j++) {
array[i] = tempArray[j];
}
}
public static void mergeSort2(int[] array) {
//每个归并段里的数据的个数是以2倍形式增长
//最少是1组,最多是array.length组
for (int i = 1; i < array.length; i *= 2) {
merge2(array, i);
}
}
3. 排序算法时间、空间复杂度总结
- 下图是各个排序算法的时间复杂度、空间复杂度及稳定性分析: