(1)插入排序
1.第i趟,设前i个元素构成的{a0,a1,。。。,an-1}的适当位置,使插入后的子序列认识排序的,ai的插入位置由关键字比较大小确定。
2.重复执行1操作,n个元素共需要进行n-1趟排序,每一行将第一个元素ai插入前面的子序列。
关键字序列{32,26,87,72,26*,17}的直接插入排序(升序)过程如图所示,以“*”区别两个关键字相同的元素,其中{}表示一个排序的子序列。
详细的基本步骤:
- 从第一个元素开始,该元素可以认为是已经被排序的;
- 取出下一个元素,再已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于心元素,将该元素移动到下一个位置
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置
- 将新元素插入到该位置后
- 重复步骤2~5
public class InsertionSort {
public static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int val = arr[i], j = i;
while (j > 0 && val < arr[j - 1]) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = val;
}
}
public static void main(String[] args) {
int[] array ={12,11,13,5,6};
insertionSort(array);
System.out.print("插入排序:");
for (int i:array){
System.out.print(i+" ");
}
}
}
运行结果
插入排序:12 11 13 5 6
时间复杂度分析:最坏情况下为O(N*2),此时待排序列为逆序,或者说接近逆序
最好情况下为O(N),此时待排序列为升序,或者说接近升序。
空间复杂度分析:只是开辟了一个 tmp 的变量 i,j,常数,即空间复杂度:O(1)
稳定性:稳定
该排序再数据越接近有序的情况,时间效率越高。
(2)希尔排序
希尔排序又称为缩小增量排序,其基本思如是分组的直接插入排序。
由直接插入排序算法分析可知,若数据序列若接近有序,那么时间效率越高;再者,当元素数量n越小时,时间效率也越高。希尔排序就是基于这两点对直接插入排序算法进行改进。
- 将一个数据序列分成若干组,每组由若干相隔一段距离(称为增量)的元素组成,在一个组内采用直接插入排序算法进行排序。
- 增量初值通常为数据序列长度的一半,以后每趟增量减半,最后增值为1,随着增量逐渐减小,组数也逐渐减小,组数也减少,组内元素个数增加,数据序列接近有序。
- 到最后再进行一次直接插入排序,排序完成
public class Text {
public static void main(String[] args) {
int a[]={3,23,33,11,14,54,66,34,32,44};
int count=0;//比较次数
for (int gap = a.length/2; gap >0 ; gap=gap/2) {
//将整个数组分为若干个子数组
for (int i = gap; i <a.length ; i++) {
for (int j =i-gap;j>=0;j=j-gap){
//交换元素
if (a[j]>a[j+gap]){
int temp =a[j];
a[j]=a[j+gap];
a[j+gap]=temp;
count++;
}
}
System.out.println(count);
System.out.println("希尔排序为:"+Arrays.toString(a));
}
}
}
}
运行结果
希尔排序为:[3, 11, 14, 23, 32, 33, 34, 44, 54, 66]
希尔排序的时间复杂度为O(n^2),但实际上它的性能比插入排序要好得多,特别是在大型列表上。希尔排序的性能取决于间隔序列的选择,但是目前还没有一种最优的间隔序列。
希尔排序算法的时间复杂度比较复杂,实际所需的时间取决于具体的增量序列。希尔排序算法的空间复杂度为O(1)
(3)选择排序
算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置;
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾;
- 重复第2步,直到所有元素均排序完毕。
import java.util.Arrays; public class InsertionSort { public static void select(int[] key){ for (int i = 0; i < key.length-1; i++) { int min =i;//假设这个数是最小的数 for (int j = i+1; j <key.length ; j++) { if(key[j]<key[min]){//从后面扫描存不存在比它更小的,如果存在就记录本趟最小的下标位置 min=j; if(min!=i){ swap(key,i,min); } } } } } private static void swap(int[] key, int i, int j) { int temp =key[j]; key[j]=key[i]; key[i]=temp; } public static void main(String[] args) { int[] key = {11, 21, 22, 33, 44, 5, 6}; InsertionSort.select(key); System.out.println(Arrays.toString(key)); } }
运行结果
选择排序:[6, 5, 11, 21, 22, 33, 44]
(4)冒泡排序
算法描述:比较两个相邻的元素大小,如果为反序,则交换,。如果按上升排序,每一趟将数据序列中的最大元素交换到最后的位置,就像气泡从水里冒出一样。
算法步骤:
- 比较相邻的元素,如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素作同样的比价,从开始第一对到结尾的最后一对,这样在最后的元素就是最大的数;
- 针对所有的元素重复以上的步骤,除了数组最后已经排好序的数组;
- 重复步骤1~3,直到排序完成。
public class InsertionSort {
public static void bubbleSort(int[] arr) {
int n = arr.length;
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j+1]) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
public static void main(String[] args) {
int[] arr ={22,31,21,2,3,5};
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
}
运行结果
[2, 3, 5, 21, 22, 31]
稳定性:稳定
时间复杂度:O(n的平方)
空间复杂度:O(1)
对数据敏感,如果一趟没有发生数据交换,则说明元素已经有序
(5)快速排序
快速排序算法描述:在数据序列总选择一个元素作为基准值,每趟从数据序列的两端开始交替进行,将小于基准值的元素交换到序列的前端,将大于基准值的元素交换到序列后端,介于两者之间的位置称为基准值的最终位置。同时,序列被划分成为两个子序列,再分别对两个子序列进行快速序列,直到子序列的长度为1,则完成排序。
算法步骤:
- 从序列中随机挑出一个元素,做为基准(pivot,这里选择序列的最左边元素作为基准);
- 重新排列序列,将所有比基准值小的元素摆放在基准前面,所有比基准值大的摆在基准的后面。该操作结束之后,该基准就处于数列的中间位置。这个操作称为分区(partition);
- 递归地把小于基准值元素的子序列和大于基准值元素的子序列进行上述操作即可
具体步骤:
- 设i,j下标分别从子序列的前后端开始
- 选取子序列第一个元素keys[i]即39作为基准值x,空出key[i]的位置
- 在子序列后端寻找小于基准值的原数,交换到序列前端。看图得知26比基准值小,则将位置锁定在26。
- 在子序列前端寻找大于基准值的原数,交换到序列后端,看图得知97比基准值大,则将位置锁定在96。
- 然后26和96这两个位置进行交换
- 最后只要不断重复这上面这几个过程就行了,在子序列中不断的进行块数排序。
代码实现
public class Text {
public static void main(String[] args) {
int a[]={3,23,33,11,14,54,66,34,32,44};
quicksort(a,0,a.length-1);
System.out.println(Arrays.toString(a));
}
private static void quicksort(int[] a, int low, int high) {
int i,j;
if(low>high){
return;
}
i =low;
j =high;
int temp=a[low];//基准位,low=length时会报错,会报异常,java.lang.ArrayIndexOutOfBoundsException
while(i<j){
while (temp <= a[j] && i < j) {
//先从右边开始向左递归,找到比temp小的值开始停止
j--;
}
//再看左边向右递增,找到比temp大的值才停止
while (temp >= a[i] && i < j) {
i++;
}
//满足i<j就交换,继续循环while(i<j)
if (i < j) {
int t = a[i];
a[i] = a[j];
a[j] = t;
}
}
a[low]=a[i];
a[i]=temp;
//左递归
quicksort(a,low,j-1);
quicksort(a,j+1,high);
}
//最后将基准位跟a[i]和a[j]相等的位置,进行交换,此时i=j
}
- 最好情况,每趟排序将序列分成长度相近的两个子序列,时间复杂度为O(nlogn)。
- 最坏情况, 每趟将序列分成长度差异很大的两个子序列,时间复杂度O(n平方)。
- 空间复杂度低:它不需要额外的空间存储,除了递归所需调用的栈空间。
(6)归并排序
归并排序的步骤:
- 分解:将数组分下两半,直到子数组只有一个元素或为空。如果数组只有一个元素或者为空,它自然就是有序的。
- 递归排序:对每个子数递归地应用归并排序,直到子数组的大小足够小,可以直接进行排序。
- 合并:将排序好的子序列合并成更大的有序数组,这个过程需要两个指针分别指向两个子数组的其实位置,并比较它们的元素,将较小的元素放入新的数组当中,然后移动对应的指针。
代码实现:
public class test3 {
public static void mergeSort(int[] arr,int l,int r){
if (l<r){
int m =1+(r-l)/2;
mergeSort(arr,l,m);
mergeSort(arr,m+1,r);
//合并两个已经排序的子数组
merge(arr,l,m,r);
}
}
//合并两个已经排序的子数组的辅助数组
private static void merge(int[] arr, int l, int m, int r) {
int n1 =m-l+1;
int n2 =r-m;
//创建临时数组索引
int[] L =new int[n1];
int[] R =new int[n2];
for (int i =0;i<n1;i++){
L[i]=arr[l+i];
}
for (int j =0;j<n2;j++){
R[j]=arr[m+1+j];
}
//合并L[]和R[]回到arr[]
int i =0,j =0;
int k =l;
while(i<n1&&j<n2){
if (L[i]<=R[j]){
arr[k]=L[i];
i++;
}else{
arr[k]=R[j];
j++;
}
k++;
}
while(i<n1){
arr[k]=L[i];
i++;
k++;
}
//复制R[]中的元素
while(i<n1){
arr[k]=L[i];
i++;
k++;
}
while(j<n2){
arr[k]=R[j];
j++;
k++;
}
}
}
算法分析:
分解阶段:每次分解数组大小减半,直到每个子数组只有一个元素。这个过程的复杂度为O(logn)。
合并阶段:合并两个大小为n/2的数组需要O(logn)的时间,因为每次遍历整个数组。由于每次合并都会减少一个子数组,所以总共需要合并n个子数组
空间复杂度为O(n)的时间。归并算法比较稳定。
(7)堆排序
堆排序是改进的直接选择排序算法,使用最小/最大堆序列来选择最小/最大值,利用了完全二叉树的性质,提高了选择的效率。
将元素序列看作是完全二叉树的层次遍历序列,如图所示,如果任意一个结点元素大于或者小于其左右孩子结点元素,则称该序列为最小或最大堆序列,根结点值为最小或最大。根据二叉树的性质第i个结点,如果有孩子,则左孩子为第2i+1个结点,右孩子为第2i+2个结点。
创建最小堆
在直接选择排序中,求一个数据序列的最小值,必须遍历序列,在比较所有元素后才能确定最小值,时间复杂度为O(n),效率较低。
如果将该数据序列“堆”成树状,约定父母节点值比孩子结点值小/大,则根结点值最小/最大,那么求最小/最大值的时间复杂度就是O(1),效率明显提高。堆的树状结构只能是完全二叉树,因为只有完全二叉树才能顺序存储,二叉树性质将一个数据序列映射到唯一的一颗完全二叉树。
由关键字序列{81,76,19,49,97,81*,13,35}创建最小堆的过程如图
- 将一个关键字序列看成一颗完全二叉树的层次遍历序列,此时塔不是堆序列。将这颗完全二叉树最深的一颗子树调整成最小堆,该子树的根是序列第parent(n/2-1)元素;在根的两个孩子中选取较小值,并上移到子树的根
- 重复1过程,从下至上依次将每课子树调整为最小堆。如果一颗子树的根值较大,根值可下移几层,最后得到该完全二叉树的层次遍历序列是一个最小堆序列。
创建了最小堆,不仅确定了一个最小值,求最小值的时间是O(1),还调整了其他元素;下次只要比较根的两个孩子结点值,就确定了次小值。因此提高了多次最小值算法效率。
算法分析:
将一个数据序列调整为堆的时间复杂度为O(1)。
堆排序算法分以下两个阶段
- 将一个数据序列建成最小/最大堆,则根结点值最小/最大。
- 进行选择排序,每趟将最小/最大值(根结点)交换到后面,再将其余值调整成堆,以此重复,直到子序列的长度为1,排序完成
下面是实际的算法步骤
- 最小堆的根值13最小,将13交换到最后,13不参加下一趟排序,子序列右边界减一;再将以76为根的子序列调成成最小堆,只要比较跟的两个孩子结点值35与19,就能确定次小值。将根值76向下调整,将根76向下调整,经过从根到叶子结点(最远)的一条路径。
- 重复步骤1,将根值与keys[n-i]元素交换,在调整最小堆,直到子序列长度为1,排序完成。上述数据序列的堆排序(降序)过程如下,{}中的为最小堆序列。
public class test3 {
public static void swap(int[] arr,int i,int j){
int temp =arr[i];
arr[i]=arr[j];
arr[j]=temp;
}
//堆排序
public static void heapSort(int[] keys){
for(int i =keys.length/2-1;i>=0;i--){//创建最小堆,根结点值最小
sift(keys,i,keys.length-1);//堆排序,每一趟将最小值交换到最后,再调整成最小堆
}
for(int i =keys.length-1;i>0;i--){
swap(keys,0,i);
sift(keys,0,i-1);
}
}
private static void sift(int[] keys, int parent, int end) {
int child =2*parent+1;//child是孩子的左节点
int x =keys[parent];//当前子树的原根值
while(child<=end){//沿着较小值孩子节点向下筛选
if(child<end&&keys[child+1]<keys[child]){//若右孩子值更小
child++;//child记住孩子值较小者
}
if (x>keys[child]){//若父母节点值更大
keys[parent]=keys[child];//将较小值结点值向上移动
child =2*parent+1;
}
else{
break;
}
}
}
}
排序算法性能比较:
以上介绍了插入,交换,选择,排序和递归等算法,直接插入排序,冒泡排序,选择排序等算法的时间复杂度为O(n),这些算法简单易懂,思路清楚,算法结构为两重循环,共进行n-1趟,每趟排序将一个元素移动到排序后的位置。数据比较合适移动在相邻的两个元素之间进行,每一趟排序与上一趟之间存在较多重复的比较,移动,交换,因此排序效率较低。
另一类较快的排序算法有希尔排序,快速排序,堆排序以及归并排序,这些算法设计各有巧妙之处,他们的共同点是:与较远的元素进行比较,数据移动较远,跳跃式地向目的地向前进,避免了许多重复的比较与移动。