一、常见排序算法:
1、插入类排序:
(1)直接插入排序
(2)希尔排序
2、选择类排序
(1)简单选择排序
(2)堆排序
3、交换类排序
(1)冒泡排序
(2)快速排序
4、归并排序
5、基数排序
二、内部排序:只考虑数据量较小仅需要使用内存的排序算法
三、稳定与非稳定:如果一个排序算法能够保留数组中重复元素的相对位置则可以被称为是稳定的。反之,则是非稳定的。
四、直接插入排序
1、基本思想
通常人们整理扑克牌的方法是一张一张的来,将每一张牌插入到其他已经有序的牌中的适当位置。在计算机的实现中,为了要给插入的元素腾出空间,我们需要将其余所有元素在插入之前都向右移动一位。
2、算法描述
一般来说,插入排序都采用就地(in-place)在数组上实现。具体算法描述如下:
(1)从第一个元素开始,该元素可以认为已经被排序
(2)取出下一个元素,在已经排序的元素序列中从后向前扫描
(3)如果该元素(已排序)大于新元素,将该元素移到下一位置
(4)重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
(5)将新元素插入到该位置后
(6)重复步骤2~5
3、Java实现
public class DirectSort {
/*
* @desc: 通过交换进行插入排序,借鉴冒泡排序
* @param a: 待排序数组
* @return: void
*/
public void sort(int[] a){
for (int i = 0; i < a.length-1; i++) {
for (int j =i+1; j >0 ; j--) {
if (a[j]0 && num
4、复杂度分析
平均时间复杂度 |
最好情况 |
最坏情况 |
空间复杂度 |
O(n²) |
O(n²) |
O(n²) |
O(1) |
5、总结与思考
插入排序所需的时间取决于输入元素的初始顺序。例如,对一个很大且其中的元素已经有序(或接近有序)的数组进行排序将会比随机顺序的数组或是逆序数组进行排序要快得多。
也称 递减增量排序算法,是插入排序的一种更高效的改进版本。希尔排序是 非稳定排序算法。希尔排序是基于插入排序的以下两点性质而提出改进方法的:
希尔排序是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。
1、基本思想
将待排序数组按照步长gap进行分组,然后将每组的元素利用直接插入排序的方法进行排序;每次再将gap折半减小,循环上述操作;当gap=1时,利用直接插入,完成排序。
可以看到步长的选择是希尔排序的重要部分。只要最终步长为1任何步长序列都可以工作。一般来说最简单的步长取值是初次取数组长度的一半为增量,之后每次再减半,直到增量为1。
2、算法描述
(1)选择一个增量序列 t1,t2,……,tk,其中 ti > tj, tk = 1;
(2)按增量序列个数 k,对序列进行 k 趟排序;
(3)每趟排序,根据对应的增量 ti,将待排序列分割成若干长度为 m 的子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
3、Java实现
/*
* @desc: 希尔排序( 递减增量排序算法)
* @param a: 待排序数组
* @return: void
*/
public void hillSort(int[] a){
int length = a.length;
int h = 1;
while (h < length/3){
h = 3 * h + 1;
}
for (;h>=1;h/=3){
for (int i = 0; i < a.length - h; i+=h) {
for (int j = i+h; j > 0 ; j -=h) {
if (a[j]< a[j-h]){
int temp = a[j];
a[j] = a[j-h];
a[j-h] = temp;
}
}
}
}
}
4、复杂度分析
平均时间复杂度 |
最好情况 |
最坏情况 |
空间复杂度 |
O(nlog2 n) |
O(nlog2 n) |
O(nlog2 n) |
O(1 |
5、总结与思考
希尔排序更高效的原因是它权衡了子数组的规模和有序性。排序之初,各个子数组都很短,排序之后子数组都是部分有序的,这两种情况都很适合插入排序。
1、基本思想
选择排序(Selection sort)是一种简单直观的排序算法。它的工作原理如下。首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n个元素的表进行排序总共进行至多 n-1 次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。
2、算法描述
(1)从未排序序列中,找到关键字最小的元素
(2)如果最小元素不是未排序序列的第一个元素,将其和未排序序列第一个元素互换
(3)重复1、2步,直到排序结束。
3、Java实现
/*
* @desc: 简单的选择排序
* @param a: 待排序数组
* @return: void
*/
public void selectSort(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]
4、复杂度分析
平均时间复杂度 |
最好情况 |
最坏情况 |
空间复杂度 |
O(n²) |
O(n²) |
O(n²) |
O(1) |
5、总结与思考
选择排序的简单和直观名副其实,这也造就了它”出了名的慢性子”,无论是哪种情况,哪怕原数组已排序完成,它也将花费将近n²/2次遍历来确认一遍。即便是这样,它的排序结果也还是不稳定的。 唯一值得高兴的是,它并不耗费额外的内存空间。
1、定义:
n个元素的序列{k1,k2,..,kn}
当且仅当满足下关系时,称之为堆。
把此序列对应的二维数组看成一个完全二叉树。那么堆的含义就是:完全二叉树中任何一个非叶子节点的值均不大于(或不小于)其左,右孩子节点的值。 由上述性质可知大顶堆的堆顶的关键字肯定是所有关键字中最大的,小顶堆的堆顶的关键字是所有关键字中最小的。因此我们可使用大顶堆进行升序排序, 使用小顶堆进行降序排序。
2、基本思想
以大顶堆为例,堆排序的过程就是将待排序的序列构造成一个堆,选出堆中最大的移走,再把剩余的元素调整成堆,找出最大的再移走,重复直至有序。
3、算法描述
(1)先将初始序列K[1..n]建成一个大顶堆, 那么此时第一个元素K1最大, 此堆为初始的无序区.
(2)再将关键字最大的记录K1 (即堆顶, 第一个元素)和无序区的最后一个记录 Kn 交换, 由此得到新的无序区K[1..n−1]和有序区K[n], 且满足K[1..n−1].keys⩽K[n].key
(3)交换K1 和 Kn 后, 堆顶可能违反堆性质, 因此需将K[1..n−1]调整为堆. 然后重复步骤2, 直到无序区只有一个元素时停止。
堆排序需要两个过程:
一是:建立堆
二是:堆顶与堆的最后一个元素交换位置
所以堆排序有两个函数组成:
一是:建堆函数
二是:反复调用建堆函数以选择出剩余未排元素中最大的数来实现排序的函数。
4、具体的操作:
(1)大顶堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
(2)创建大顶堆(Build_Max_Heap):将堆所有数据重新排序
(3)堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算
5、对于堆节点的访问:
(1)父节点i的左子节点在位置:(2*i+1);
(2)父节点i的右子节点在位置:(2*i+2);
(3)子节点i的父节点在位置:floor((i-1)/2);
6、Java实现
/**
* @Description: 堆排序
* @Version:
*/
public class HeapSort {
/*
* @desc: 建堆函数---将数组堆化
* @param a:待排序数组
* @param n: 非叶子结点的索引
* @return: void
*/
public 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;
}
}
}
/*
* @desc: 堆排序
* @param a: 待排序数组
* @return: void
*/
public void heapSort(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;
}
}
}
测试代码
import org.junit.Test;
import static org.junit.Assert.*;
public class HeapSortTest {
@Test
public void fun(){
HeapSort hs = new HeapSort();
int[] a = {12,45,10,8,89,65,32,189,456,98,21,33,3,78,6};
System.out.println("排序前的数组:");
for (int i = 0; i < a.length; i++) {
System.out.printf("%4d",a[i]);
}
System.out.println();
long startTime = System.currentTimeMillis(); //获取开始时间
hs.heapSort(a);
long endTime = System.currentTimeMillis(); //获取结束时间
System.out.println("排序耗时:"+(endTime-startTime)+"ns");
System.out.println("排序后的数组:");
for (int i = 0; i < a.length; i++) {
System.out.printf("%4d",a[i]);
}
System.out.println();
}
}
7、复杂度分析
(1)建立堆的过程, 从length/2 一直处理到0, 时间复杂度为O(n);
(2)调整堆的过程是沿着堆的父子节点进行调整, 执行次数为堆的深度, 时间复杂度为O(lgn);
(3)堆排序的过程由n次第2步完成, 时间复杂度为O(nlgn).
平均时间复杂度 |
最好情况 |
最坏情况 |
空间复杂度 |
O(nlog2n) |
O(nlog2n) |
O(nlog2n) |
O(1) |
8、总结与思考
由于堆排序中初始化堆的过程比较次数较多, 因此它不太适用于小序列。 同时由于多次任意下标相互交换位置, 相同元素之间原本相对的顺序被破坏了, 因此, 它是不稳定的排序。
1、基本思想
冒泡排序(Bubble Sort)是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
2、算法描述
(1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
(3)针对所有的元素重复以上的步骤,除了最后一个。
(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
3、Java实现
/**
* @Description: 冒泡排序
* @Version:
*/
public class BubbleSort {
public static void sort(int[] a){
//外循环控制比较的次数(趟数)
for (int i = 0; i < a.length; 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;
}
}
}
}
}
4、复杂度分析
平均时间复杂度 |
最好情况 |
最坏情况 |
空间复杂度 |
O(n²) |
O(n) |
O(n²) |
O(1) |
冒泡排序是最容易实现的排序, 最坏的情况是每次都需要交换, 共需遍历并交换将近n²/2次, 时间复杂度为O(n²). 最佳的情况是内循环遍历一次后发现排序是对的, 因此退出循环, 时间复杂度为O(n). 平均来讲, 时间复杂度为O(n²). 由于冒泡排序中只有缓存的temp变量需要内存空间, 因此空间复杂度为常量O(1).
5、总结与思考
由于冒泡排序只在相邻元素大小不符合要求时才调换他们的位置, 它并不改变相同元素之间的相对顺序, 因此它是稳定的排序算法。
1、基本思想
快速排序的基本思想:挖坑填数+分治法。
快速排序使用分治法策略来把一个序列(list)分为两个子序列(sub-lists)。
快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
2、算法描述
(1)从数列中挑出一个元素,称为"基准"(pivot)。
(2)重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
(3)递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序。
3、Java实现
public class MergerSort {
/*
* @desc:
* @param a: 待排序数组
* @param low: 子序列的左边元素的下标
* @param high: 子序列右边元素的下标
* @return: void
*/
public static void sort(int[] a,int low,int high){
//已经排完
if (low >= high){
return;
}
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; //放置基准值,准备分治递归快排
sort(a,low,left - 1);
sort(a,left + 1,high);
}
}
4、复杂度分析
平均时间复杂度 |
最好情况 |
最坏情况 |
空间复杂度 |
O(nlog₂n) |
O(nlog₂n) |
O(n²) |
O(1) |
1、基本思想
归并排序算法是将两个(或两个以上)有序表合并成一个新的有序表,即把待排序序列分为若干个子序列,每个子序列是有序的。然后再把有序子序列合并为整体有序序列。
2、算法描述:
归并排序可通过两种方式实现:
递归法(假设序列共有n个元素):
(1)将序列每相邻两个数字进行归并操作,形成 floor(n/2)个序列,排序后每个序列包含两个元素;
(2)将上述序列再次归并,形成 floor(n/4)个序列,每个序列包含四个元素;
(3)重复步骤2,直到所有元素排序完毕。
迭代法
(1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
(2)设定两个指针,最初位置分别为两个已经排序序列的起始位置
(3)比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
(4)重复步骤3直到某一指针到达序列尾
(5)将另一序列剩下的所有元素直接复制到合并序列尾
3、Java实现:
归并排序其实要做两件事:
因此,归并排序实际上就是两个操作,拆分+合并
public class Merger {
//归并所需的辅助数组
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个条件判断:
* - 左半边用尽(取右半边的元素)
* - 右半边用尽(取左半边的元素)
* - 右半边的当前元素小于左半边的当前元素(取右半边的元素)
* - 右半边的当前元素大于等于左半边的当前元素(取左半边的元素)
* @param a
* @param low
* @param mid
* @param high
*/
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++];
}
}
}
}