整理了常见的7种排序算法,分一下类即:
插入排序: 直接插入和希尔排序
选择排序:简单选择和堆排序
交换排序:冒泡排序和快速排序
此外,还有一种归并排序。
(一)插入排序
(1)直接插入排序
插入排序:时间复杂度是O(n^2) 稳定
思路记忆:分为有序列和无序列两个部分,前面部分是有序列(从一个元素开始),然后向其中插入
public class InsertSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = sc.nextInt();
}
new InsertSort().insertSort(array);
}
public void insertSort(int[] array) {
int length = array.length;
// 将数组第一个元素驻留,作为有序列部分,从第二个元素开始作为插入元素
for (int i = 1; i < length; i++) {
// 有序列中最后一个元素的下标
int j = i - 1;
// 将要插入到元素
int temp = array[i];
// 将要插入的元素和有序列比较,比较时从后向前,这样比较的结果有两种,一种是找到了一个比有序列中元素
// 大或者等于的位置,另一种是比有序列中第一个元素即最小的元素还要小。在此之前全体后移,给要插入的元素留出array【j】的位置
for (; j >= 0 && array[j] > temp; j--) {
array[j + 1] = array[j];
}
// 这里加一是因为for循环将理想位置又减了1
array[j + 1] = temp;
}
for (int i : array)
System.out.print(i + " ");
}
//可以看到,当遇到相等的情况就会停下来,不再往前移,所以稳定。
//for循环嵌套,所以时间复杂度是O(n)
//没有使用辅助空间
}
(2)希尔排序
1. 希尔排序是对直接插入排序算法的优化。为什么这样说呢?是因为直接插入排序是将前面部分视作有序列,然后将后面的元素挨个插入
而希尔排序进行了分组:分为间隔为h的几队,这几组相同位置的元素构成一组,组内进行比较,也就是通过组内的直接插入排序,使其初步有序,
而且,这样需要移动的元素数目大大减少,当后期h逐步变小时,虽然移动的数目变多了,但是这时候已经基本有序了,移动的可能性就变小了。
2. 关于希尔排序的时间复杂度,这与h的选择有关,是一个未解的数学之谜。大致在O(nlogn)~O(n^2)之间
希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)),因此中等大小规模表现良好,对规模非常大的数据排序不是最优选择。
但是比O( )复杂度的算法快得多。并且希尔排序非常容易实现,算法代码短而简单。
此外,希尔算法在最坏的情况下和平均情况下执行效率相差不是很多,与此同时快速排序在最坏的情况下执行的效率会非常差。
专家们提倡,几乎任何排序工作在开始时都可以用希尔排序,若在实际使用中证明它不够快,再改成快速排序这样更高级的排序算法.
3. 希尔排序是不稳定的。
public class ShellSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = sc.nextInt();
}
new ShellSort().shellSort(array);
for (int i : array)
System.out.print(i + " ");
}
public void shellSort(int[] array) {
int i, j, gap;
int length = array.length;
for (gap = length / 2; gap > 0; gap--) {
//组内的直接插入排序
for (j = gap; j < length; j++) {
//组内,从第二个元素开始找自己合适插入的位置,如果比前面的小,那么往前移,一直移到合适位置再落地生花
int temp = array[j];
int k = j - gap;
while (k >= 0 && array[k] > temp) {
array[j] = array[k];
k -= gap;
}
array[k + gap] = temp;
}
}
}
//学到的一种简洁的写法
public void simpleShell(int[] array){
int i,j, gap;
for(gap = array.length/2; gap >0;gap /=2){
//组内从第二个元素开始找自己的合适位置
for( i = gap;i<array.length;i++)
for(j = i-gap;j >= 0 && array[j] > array[j+gap]; j-= gap)
Swap(array[j],array[j+gap]);
}
}
private void Swap(int i, int j) {
int temp = i;
i = j;
j = temp;
}
}
(二) 选择排序
(1)简单选择排序
直接选择排序也叫简单选择排序,即每次选出最小的放到前面的序列中。
直接选择排序要进行n-1次扫描,每次扫描都要选出无序区中最小的,并将前面位置的东西换到最小值位置上。也就是说,最好情况和最坏情况下,
虽然我们人眼能看出来是有序的了,仍然要进行扫描和比较。所以最好、最坏、平均时间复杂度都是O(n)。
再者,直接选择排序是不稳定的,eg :
排序前: 2,4,4*,3
排序后: 2 3 4* 4
public class SelectSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = sc.nextInt();
}
new SelectSort().selectSort(array);
}
public void selectSort(int[] array) {
int length = array.length;
//遍历n-1次
for(int i = 0;i<length-1;i++ ){
int min = i;
//从无序队中,找到最小元素的下标,存在min中
for(int j = i+1;j<length;j++){
if(array[j] < array[min])
min = j;
}
//交换当前位置和最小值位置的元素,即将后面无序区中的最小值加到前面的有序列中
int temp = array[i];
array[i] = array[min];
array[min] = temp;
}
for(int i : array)
System.out.print( i + " ");
}
}
(2)堆排序
堆排序是利用大根堆或者小根堆的建立与调整进行排序,每次将root 与堆的最后一个元素交换,实现一个元素就位,进而调整剩下堆的元素
堆排序是不稳定的排序算法,其平均时间复杂度是O(nlog n )
public class HeapSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = sc.nextInt();
}
new HeapSort().heapSort(array);
}
//往下的方向
public void maxHeap(int[] array, int length,int i){
int left = 2*i +1;
int right = 2* i+2;
//找到根节点及其左右孩子中最大的保存在largest中
int largest = i;
if(left < length && largest < left)
largest = left;
if(right < length && largest < right)
largest = right;
//如果根节点不是最大的,即需要调整成最大堆,就将根节点和孩子交换
if(largest != i){
int temp = i;
i = largest;
largest = temp;
}
maxHeap(array, length, largest);
}
//从下往上的方向
public void buildMaxHeap(int[] array){
int half = array.length/2;
for(int i = half; i>=0;i--){
//以每一个非叶子节点为根节点的子树都要建成一个大根堆
maxHeap(array,array.length,i);
}
}
public void heapSort(int[] array){
buildMaxHeap(array);
//array[0] 是大根堆的堆顶,每次要将堆顶换到后面去,再把剩下的调整为大根堆,重复此过程,直到堆里还剩下一个元素
for(int i = array.length-1; i >=1;i--){
int temp = array[0];
array[0] = array[i];
array[i] = temp;
maxHeap(array, i, 0);
}
}
}
(三)交换排序
(1)冒泡排序
冒泡排序是对数组进行n次扫描,每次都将最大的一个放到最右边
最好情况即本来就有序,这样扫描n次但无移动,所以时间复杂度是O(n)
最坏情况即反序,需要进行n次扫描,每次扫描都要进行n-i次移动,时间复杂度是O(n^2)
所以平均时间复杂度是O(n^2)
当遇到前后两个元素相等时,我们是不会去交换他们的,所以冒泡排序是稳定的
public class BubbleSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = sc.nextInt();
}
new BubbleSort().bubbleSort(array);
}
public void bubbleSort(int[] array) {
int length = array.length;
// 进行 n 次遍历
for (int i = 0; i < length; i++) {
// 除了已经到位的右边的元素,其他的再从头遍历比较 其中界限 length-1-i 是关键点
for (int j = 0; j < length - 1 - i; j++) {
// 如果前面的元素比后面的大,就交换两者
if (array[j] > array[j + 1]) {
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}
for (int i : array)
System.out.print(i + " ");
}
}
(2)快速排序
1.快速排序是对冒泡排序的优化,冒泡排序每一次扫描进行的比较只能保证一个元素就位,但是快速排序用了分治的思想,能让
一堆元素初始就位。
2.快速排序的思路是: 选出一个中间元素(一般选数列中第一个元素即可),然后通过从后向前查找和从前向后查找两种方式,使比中间元素小的元素在中间元素的左边
比中间元素大的元素在其右边。接下来再将左侧部分和右侧部分用相同的方法进行分治。
3.快速排序的时间复杂度:每次将待排序数组分为两部分,理想状态下,每一次都是划分为等长的两部分,
因此进行log2 n 次划分,每次两部分共进行n次比较,因此最好时间复杂度是O(nlog2 n)
最坏情况是数组已经有序或者基本有序的情况,每次划分只能减少一个元素,快速排序退化为冒泡排序,时间复杂度是O(n^2)
由此,我们也可以看出来,对于快速排序,原有数列越乱越好。
4.快速排序的空间复杂度:O(1) 但要注意在递归栈上需要花费最少logn 最多n的空间
public class QuickSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = sc.nextInt();
}
new QuickSort().quickSort(array, 0, array.length - 1);
}
public int Split(int[] array, int low, int high) {
//split作为中间分割元素
int split = array[0];
//双while结构
while (low < high) {
//从后往前找到第一个比分割元素小的元素下标
while (low < high && array[high] >= split)
high--;
//将此元素换到分割元素 前,low指针往后移,此时先不必让high处的元素固定下来,后面还会更改
if (low < high)
array[low++] = array[high];
//从前往后找到第一个比分割元素大的元素下标
while (low < high && array[low] <= split)
low++;
//将此元素换到分割元素 后,high指针往前移,此时先不必让low处的元素固定下来,后面还会更改
if (low < high)
array[high--] = array[low];
}
//最后将分割元素归位
array[low] = split;
return low;
}
public void quickSort(int[] array, int low, int high) {
int partition;
if (low < high) {
//分治法
partition = Split(array, low, high);
//快排两侧部分
quickSort(array, low, partition - 1);
quickSort(array, partition + 1, high);
}
}
}
(四)归并排序
(1)归并排序也是使用了分治思想。这里我们可以和同样使用分治思想的快速排序进行比较可以发现,
快速排序是先整体初步有序再细分有序,而归并排序是先细分有序,再合并到整体有序。两种有异曲同工之妙
(2)归并排序的思路是,先两两合并成有序列,得到的一个个子序列再作为一个个小整体两两合并
具体来讲,第一步,申请辅助数组空间,和待排数组等长,用来存放排序后的
第二步,设置头指针和尾指针,最初位置是待排序列的头和尾
第三步,比较两个指针指向的元素。选择相对小的元素放入合并空间,并移动指针到下一位置
重复第三步知道某一指针超出序列,将另一序列剩下的所有元素直接copy到合并序列尾部
(3)示例:如 设有数列{6,202,100,301,38,8,1}
初始状态:6,202,100,301,38,8,1
第一次归并后:{6,202},{100,301},{8,38},{1},比较次数:3;
第二次归并后:{6,100,202,301},{1,8,38},比较次数:4;
第三次归并后:{1,6,8,38,100,202,301},比较次数:4;
总的比较次数为:3+4+4=11,;
(4)时间复杂度:归并排序是仅次于快速排序的排序算法,最好,最坏和平均时间复杂度都是O(nlog n)
(5)空间复杂度:O(n)
(6)归并排序是稳定的排序算法
public class MergeSort {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int[] array = new int[n];
for (int i = 0; i < n; i++) {
array[i] = sc.nextInt();
}
new MergeSort().mergeSort(array, 0, array.length - 1);
for (int i : array)
System.out.print(i + " ");
}
public int[] mergeSort(int[] array, int low, int high) {
//low == high 这是递归调用退出的条件
if(low < high){
int mid = (low + high) / 2;
//对左半部分归并排序
mergeSort(array, low, mid);
//对右半部分归并排序
mergeSort(array, mid+1, high);
//合并两者排序后的有序列
merge(array,low,mid,high);
}
return array;
}
public void merge(int[] array,int low,int mid,int high){
//辅助数组空间用于储存排序后的序列
int[] temp = new int[high-low+1];
//左边子序列的头
int i = low;
//右边子序列的头
int j = mid +1;
//辅助数组的下标初始值
int k = 0;
//两个子序列还没有一个完成排序时,每次两个子序列中最小的进入辅助数组中
while(i<=mid && j<= high){
if(array[i] < array[j])
temp[k++] = array[i++];
else
temp[k++] = array[j++];
}
//必然会有一个子序列先为空,这时将剩下的一个子序列剩下的元素复制进辅助数组中去
while(i<=mid)
temp[k++] = array[i++];
while(j<= high)
temp[k++] = array[j++];
//将辅助数组的内容搬回原来的数组中去
for( int l = 0;l<temp.length;l++)
array[low++] = temp[l];
}
}
附:各种排序算法时间复杂度统计表
具有稳定性的算法有: 基数排序 直接插入排序 冒泡排序 归并排序
可以快速记忆:奇数只鱼儿插入水中冒出气泡,不一会水面又归于了平静。