排序分为内部排序和外部排序(外部存储)
常见的七大排序,这些都是内部排序 。
1、插入排序:直接插入排序
1、插入排序:每次将一个待排序的记录,按其关键字的大小插入到前面已排序好的记录序列 中的适当位置,直到全部记录插入完成为止。稳定排序算法
一个排序算法的稳定性与不稳定性是通过排序后相同元素的先后顺序 来判断的。
稳定性:如果排序前后,具有相同关键字的元素的相对顺序没有改变,则排序算法被认为是稳定的。
不稳定性:如果排序前后,具有相同关键字的元素的相对顺序发生了改变,则排序算法被认为是不稳定的。
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后往前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后(这就保证了相同元素的顺序和排序前一样,所以是稳定排序 )
- 重复扫描
2、代码
public class InsertSort {
/**
* <>
* @method: insertSort
* @Param: [arr]
* @Return: void
* @exception:
* @Author: fsy
* @Date: 19-5-29 下午3:50
* @description:
*
*/
public void insertSort(int[] arr){
//需要插入的数
int insertNum;
for (int i=1; i <arr.length ; i++) {
insertNum=arr[i];
//序列元素个数
int j=i-1;
//将大于insertNum的元素往后移动
while (j>=0&&arr[j]>insertNum){
arr[j+1]=arr[j];
j--;
}
//找到位置,插入当前元素
arr[j+1]=insertNum;
}
}
3、复杂度分析
时间复杂度:
- O(n²)
空间复杂度:
- O(1)
4、总结:
插入排序所需的时间取决于输入元素的初始顺序 。对于一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数据或是逆序数据进行排序要快的多
2、插入排序:希尔排序
1、希尔排序:希尔排序的本质就是分组插入排序 ,又称缩小增量法。将整个无序序列分割成若干个子序列(由相隔某个“增量”的元素组成)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序时,再对全体元素进行一次直接插入排序 。因为进行直接插入排序时元素基本有序,所以效率是很高的,因此希尔排序在时间效率上有很大提高。希尔排序是不稳定排序算法
2、代码
public void shellSort(int[] d) { //d[]为增量数组
RecordNode temp;
int i, j;
System.out.println("希尔排序");
//控制增量,增量减半,若干趟扫描
for (int k = 0; k < d.length; k++) {
//一趟中若干子表,每个记录在自己所属子表内进行直接插入排序
int dk = d[k];
for (i = dk; i < this.curlen; i++) {
temp = r[i];
for (j = i - dk; j >= 0 && temp.key.compareTo(r[j].key) < 0; j -= dk) {
r[j + dk] = r[j];
}
r[j + dk] = temp;
}
System.out.print("增量dk=" + dk + " ");
}
}
3、复杂度分析
时间复杂度:
- O(nlog2 n)
空间复杂度:
- O(1)
4、总结
希尔排序更高效是因为它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序
3、选择排序:简单选择排序
1、选择排序:工作原理如下。首先在未排序序列中找到最小(大)元素,存放在排序序列的初始位置。然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序列的末尾,以此类推,直到所有元素排序完毕。选择排序的主要优点是与数据移动有关,如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对n个元素的表进行排序最多n-1次交换。不稳定排序
2、代码:
public static void sort(int[] a) {
for (int i = 0; i < a.length; i++) {
int min = i;
//选出之后待排序中值最小的位置
for (int j = i + 1; j < a.length; j++) {
if (a[j] < a[min]) {
min = j;
}
}
//最小值不等于当前值时进行交换
if (min != i) {
int temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
3、复杂度分析
时间复杂度:
- O(n²)
空间复杂度:
- O(1)
4、总结
选择排序非常简单和直观,但是也非常慢。无论是哪种情况,哪怕原数组已经排序完成,它也将花费将近n²/2次遍历来确认一遍。它的排序结果也是不稳定的。不耗费额外的内存空间
4、选择排序:堆排序
1、堆的定义如下:n个元素的序列(k1,k2,… ,kn)
当且仅当满足下列关系时,称之为堆
把此序列对应的二维数组看成是一个完全二叉树,那么堆的含义就是:完全二叉树中任何一个非叶子节点的值均不大于(或不小于)其左、右孩子节点的值 。由上述性质可知,大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。因此我们可以使用大顶堆进行升序排序,使用小顶堆进行降序排序。不稳定排序
基本思想:以大顶堆为例,堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序
2、代码
/**
* @param a
*/
public static void sort(int[] a) {
for (int i = a.length - 1; i > 0; i--) {
max_heapify(a, i);
//堆顶元素(第一个元素)与Kn交换
int temp = a[0];
a[0] = a[i];
a[i] = temp;
}
}
/***
*
* 将数组堆化
* i = 第一个非叶子节点。
* 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。
* 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
*
* @param a
* @param n
*/
public static void max_heapify(int[] a, int n) {
int child;
for (int i = (n - 1) / 2; i >= 0; i--) {
//左子节点位置
child = 2 * i + 1;
//右子节点存在且大于左子节点,child变成右子节点
if (child != n && a[child] < a[child + 1]) {
child++;
}
//交换父节点与左右子节点中的最大值
if (a[i] < a[child]) {
int temp = a[i];
a[i] = a[child];
a[child] = temp;
}
}
}
3、复杂度分析
时间复杂度:O(nlog₂ n)
空间复杂度:O(1)
4、总结
由于堆排序中初始化堆的过程比较次数较多,因此不太使用于小序列 。由于多次任意下标相互交换位置,相同元素之间原本相对的顺序被破坏了,是不稳定排序。
5、交换排序:冒泡排序
1、冒泡排序:是一种简单排序。重复地走访过要排序的元素,一次比较两个元素,如果他们的顺序错误就把他们交换过来,走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。只在顺序不符合大小要求时交换,所以不会破坏相同元素间的顺序,因此是稳定排序
- 比较相邻的元素,如果第一个比第二个大,就交换他们两个
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,这步做完,最后的元素会是最大的数
- 针对所有的元素重复以上的步骤,除了最后一个
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
2、代码
public static void sort(int[] a) {
//外层循环控制比较的次数
for (int i = 0; i < a.length - 1; i++) {
//内层循环控制到达位置
for (int j = 0; j < a.length - i - 1; j++) {
//前面的元素比后面大就交换
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
3、复杂度分析
时间复杂度:
- 最好:O(n)
- 最坏:O(n²)
- 平均时间复杂度:O(n²)
空间复杂度:O(1)
4、总结:
是稳定的排序算法。
6、交换排序:快速排序
1、快速排序:使用分治法策略来把一个串行分为两个子串行。通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。不稳定排序算法
2、代码
public static void sortByStack(int[] a) {
Stack<Integer> stack = new Stack<Integer>();
//初始状态的左右指针入栈
stack.push(0);
stack.push(a.length - 1);
while (!stack.isEmpty()) {
//出栈进行划分
int high = stack.pop();
int low = stack.pop();
int pivotIndex = partition(a, low, high);
//保存中间变量
if (pivotIndex > low) {
stack.push(low);
stack.push(pivotIndex - 1);
}
if (pivotIndex < high && pivotIndex >= 0) {
stack.push(pivotIndex + 1);
stack.push(high);
}
}
}
private static int partition(int[] a, int low, int high) {
if (low >= high) return -1;
int left = low;
int right = high;
//保存基准的值
int pivot = a[left];
while (left < right) {
//从后向前找到比基准小的元素,插入到基准位置中
while (left < right && a[right] >= pivot) {
right--;
}
a[left] = a[right];
//从前往后找到比基准大的元素
while (left < right && a[left] <= pivot) {
left++;
}
a[right] = a[left];
}
//放置基准值,准备分治递归快排
a[left] = pivot;
return left;
}
3、复杂度分析
时间复杂度:
- 最好:O(nlog₂ n)
- 最坏:O(n²)
- 平均时间复杂度:O(nlog₂ n)
空间复杂度:O(1)
7、归并排序
1、归并排序:归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的,然后再把有序子序列合并为整体有序序列。稳定排序算法
归并排序可通过两种方式实现:
- 自上而下的递归
- 自下而上的迭代
2、(递归的方法)代码
public class Merge {
//归并所需的辅助数组
private static int[] aux;
public static void sort(int[] a) {
//一次性分配空间
aux = new int[a.length];
sort(a, 0, a.length - 1);
}
public static void sort(int[] a, int low, int high) {
if (low >= high) {
return;
}
int mid = (low + high) / 2;
//将左半边排序
sort(a, low, mid);
//将右半边排序
sort(a, mid + 1, high);
merge(a, low, mid, high);
}
/**
* 该方法先将所有元素复制到aux[]中,然后在归并会a[]中。方法咋归并时(第二个for循环)
* 进行了4个条件判断:
* - 左半边用尽(取右半边的元素)
* - 右半边用尽(取左半边的元素)
* - 右半边的当前元素小于左半边的当前元素(取右半边的元素)
* - 右半边的当前元素大于等于左半边的当前元素(取左半边的元素)
*/
public static void merge(int[] a, int low, int mid, int high) {
//将a[low..mid]和a[mid+1..high]归并
int i = low, j = mid + 1;
for (int k = low; k <= high; k++) {
aux[k] = a[k];
}
for (int k = low; k <= high; k++) {
if (i > mid) {
a[k] = aux[j++];
} else if (j > high) {
a[k] = aux[i++];
} else if (aux[j] < aux[i]) {
a[k] = aux[j++];
} else {
a[k] = aux[i++];
}
}
}
}
3、复杂度分析
时间复杂度:O(nlog₂n)
空间复杂度:O(n)
4、总结:主要缺点是所需的额外空间和N成正比(N为待排序数组的长度)
8、总结
各种排序性能对比:
排序类型 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
选择排序 | O(n²) | O(n²) | O(n²) | O(1) | 不稳定 |
直接插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
折半插入排序 | O(n²) | O(n) | O(n²) | O(1) | 稳定 |
希尔排序 | O(nlog2 n) | O(nlog2 n) | O(nlog2 n) | O(1) | 不稳定 |
归并排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(n) | 稳定 |
快速排序 | O(nlog₂n) | O(nlog₂n) | O(n²) | O(nlog₂n) | 不稳定 |
堆排序 | O(nlog₂n) | O(nlog₂n) | O(nlog₂n) | O(1) | 不稳定 |
计数排序 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶排序 | O(n+k) | O(n+k) | O(n²) | O(n+k) | (不)稳定 |
基数排序 | O(d(n+k)) | O(d(n+k)) | O(d(n+kd)) | O(n+kd) | 稳定 |
稳定:冒泡排序、直接插入排序、归并排序
不稳定:选择排序、希尔排序、快速排序、堆排序