1.冒泡排序
冒泡排序是一种交换排序(相邻元素两两比较)。
算法思想:冒泡排序重复地走访过要排序的数列,一次比较相邻的两个元素,如果它们的顺序不符合次序要求就交换它们的位置。走访数列的工作是重复地进行直到没有再需要交换的元素时,就说明该数列已经排序完成。
假设一个数组中有N个元素,要求按照升序排序。
(1)如果采用从后往前比较的方法,则最小值率先出现在数组的头角标位上。
//假设数组一共有N个元素,要求按照升序排序
//冒泡排序1(如果是从后往前比较,则最小值率先出现在数组的头角标位上)
public void bubbleSort1(int[] arr)
{
int tmp = 0;//定义一个临时变量
//假设数组一共有N个元素,则最坏情况下需要进行N-1轮排序
for(int i = 0;i < arr.length - 1;i++)
{
//因为是采用从后往前比较,则从最后一个元素开始比较
//在第i轮排序时,前面的i-1个元素都已有序,只需要比较后面的N-(i-1)=N-i+1个元素即可,此时需要进行N-i+1-1=N-i次相邻元素两两比较
for(int j = arr.length - 1;j > i;j--)
{
//相邻元素两两比较,如果不符合次序要求就交换两个元素的位置
if(arr[j - 1] > arr[j])
{
tmp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = tmp;
}
}
}
}
(2)如果采用从前往后比较的方法,则最大值率先出现在数组的尾角标位上。
//冒泡排序2(如果是从前往后比较,则最大值率先出现在数组的尾角标位上)
public void bubbleSort2(int[] arr)
{
int tmp = 0;//临时变量
//假设数组一共有N个元素,在最坏情况下需要进行N-1轮排序
for(int i = 0;i < arr.length - 1;i++)
{
//因为是从前往后比较,则从数组的第一个元素开始比较
//在第i轮排序时,前面的N-i+1个元素都还没有排序,需要进行N-i+1-1=N-i次比较
for(int j = 0;j < arr.length - i - 1;j++)
{
//相邻元素两两比较,如果不符合次序要求则交换两个元素的位置
if(arr[j] > arr[j + 1])
{
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
(3)冒泡排序算法的优化:添加一个布尔型的标识位flag,用于标识在该轮排序中是否发生了数组元素的交换,如果没有发生则说明数组已经排好序了,就可以结束比较了。
//冒泡排序的优化
public void bubbleSort3(int[] arr)
{
int tmp = 0;//临时变量
//标识位,用来标识在该轮排序中是否发生了元素的交换,如果没有发生元素的交换则标识该数组已经排好序了,就不需要再进行比较了
boolean flag = false;
for(int i = 0;i < arr.length - 1;i++)
{
flag = false;
//从后往前比较,最小值率先出现在数组的头角标位上
//在第i轮排序时,前i-1个元素已经排好序了
for(int j = arr.length - 1;j > i;j--)
{
if(arr[j - 1] > arr[j])
{
tmp = arr[j - 1];
arr[j - 1] = arr[j];
arr[j] = tmp;
flag = true;
}
}
if(!flag)
break;//跳出循环
}
}
冒泡排序的特点:
(1)冒泡排序算法是通过相邻元素两两比较进行排序的,在排序过程中不会改变两个相同元素的相对位置,因此属于稳定排序。
(2)冒泡排序的最好时间复杂度是O(N),最坏时间复杂度是O(N ^ 2),因此平均实践复杂度是O(N ^ 2)。
(3)数组元素的顺序越接近正序的时候,冒泡排序算法的性能越好。
2.快速排序
算法思想:通过一趟排序将要排序的数据分割成独立的两部分,使得分割点左侧都是比它小的数据,分割点右侧都是比它大的数据。然后,再按照此方法对这两部分数据进行分别的快速排序。整个排序过程可以递归进行,以此达到使整个数据变成有序序列。
快速排序的处理过程:
假设初始状态为一组无序的数组:2 4 5 1 3
1)首先定义两个指针left和right分别指向序列的第一个元素(2)和最后一个元素(3),然后以第一个元素(序列最左边的元素2)作为基准,并将基准元素的值使用一个临时变量base存储起来。
2)从右向左扫描,移动偏移指针right,寻找第一个比基准元素base小的元素(K[right]<base),并将其值赋值给指针left所指的位置。
3)从左向右扫描,移动偏移指针left,寻找第一个比基准元素base大的元素(K[left] > base),并将其值赋值给指针right所指的位置。
4)不断重复二、三步骤,直到指针left和right重合,这样所有的元素就都被扫描了一遍了。此时,将基准元素base的值赋值给重合位置。此时,已经完成了一次排序。基准数base左边都是比他小的数,右边都是比它大的数。
5)以基准数base作为分割点,再对其左右两边的数分别按照步骤一、二、三、四去排序。经过递归的过程,最后排序结束。
//快速排序法
public void quickSort(int[] arr)
{
quickSort(arr, 0, arr.length - 1);
}
public void quickSort(int[] arr, int left, int right)
{
if(left >= right)//防止堆栈溢出
return;
int i = left;
int j = right;
int base = arr[i];//以序列中的第一个元素(最左边的元素)作为基准
while(i < j)
{
while(i < j && arr[j] >= base)//从右向左找到第一个小于base的元素K[j],使得K[j]<base,并将其移到指针i所指的位置
{
j--;
}
if(i < j)
arr[i++] = arr[j];
while(i < j && arr[i] <= base)//从左到右找到第一个大于base的元素K[i],使得K[i]>base,并将其移到指针j所指的位置
{
i++;
}
if(i < j)
arr[j--] = arr[i];
}
//当指针重合时,表示此时序列中的所有元素都被遍历过一遍了
//将base的值赋值给此时指针重合处
arr[i] = base;
//此时,在base左侧都是比它小的数,在base右侧都是比它大的数
//对base左右两侧的数据分别递归进行上述步骤,直到所有元素都排好序
quickSort(arr, left, i - 1);
quickSort(arr,i + 1,right);
}
快速排序的特点:
(1)快速排序算法属于交换排序,且是不稳定的排序算法(相等元素可能会因为分区而交换顺序)。
(2)快速排序算法的时间复杂度:最好情况和平均情况的时间复杂度是O(NlogN),最差情况的时间复杂度是O(N ^ 2)。
(3)快速排序算法的空间复杂度是O(logN)。
(4)序列元素随机分布时,快速排序算法的性能最好;序列元素越接近有序时,快速排序算法的性能越差。
3.插入排序
插入排序(每次将一个新的数据插入到有序序列的合适位置里)的处理过程:
假设初始有一组无序序列R0,R1,R2,…,RN-1
(1)首先将这个序列中下标为0的元素R0视为一个只包含有1个元素的有序序列,然后我们要依次把元素R1,R2,…,RN-1插入到这个有序序列中去。所以需要一个外部循环,从下标1开始扫描到下标N-1。
(2)假设要将元素Ri插入到前面有序的序列中,可知此时前面i-1个元素都已有序,元素Ri需要依次与前面的i-1个元素进行比较来确定插入位置。所以需要一个内部循环,从后往前,即从下标i-1扫描到下标0。
(3)在插入元素时,在插入位置处的元素及其之后的元素都需要向后顺次移动一个位置。
//直接插入排序
public void insertSort(int[] arr)
{
int tmp = 0;//用于临时存储要插入的元素的值
//因为我们将下标为0的元素R0看作是一个初始只包含1个元素的有序序列,然后再依次将元素R1,R2,...,RN-1插入到这个有序序列中
for(int i = 1;i < arr.length;i++)
{
tmp = arr[i];
int j = 0;//定义并初始化该局部变量
//当插入下标为i的元素Ri时,前面i-1个元素已经有序了,从后往前,Ri依次与前面的元素进行比较来确定合适的插入位置。
for(j = i - 1;j >= 0;j--)
{
if(arr[j] > tmp)
arr[j + 1] = arr[j];//依次向后移动一个位置
else
break;//跳出该层循环
}
arr[j + 1] = tmp;
}
}
插入排序算法的特点:
(1)当序列中的元素已经是正序排列时,只需要从前往后遍历一遍所有元素,但无需移动元素,因此最好情况下时间复杂度为O(N);当序列中的元素全部是反序排列时,除了需要从前往后遍历一遍所有元素以外,每次都需要移动元素,因此最坏情况下时间复杂度为O(N ^ 2),所以平均时间复杂度为O(N ^ 2)。因此,序列越接近于正序,插入排序算法的性能就越好。
(2)由于在排序过程中,我们只定义了一个临时变量tmp来存储将要插入的元素,因此空间复杂度为O(1)。
(3)插入排序算法是一种稳定的排序算法,因为在直接插入排序的过程中,不需要改变相等元素的相对位置。
4.希尔排序
算法思想:把序列中的所有记录按照步长gap分组,对每组记录按照直接插入排序方法进行排序。随着步长gap的减少,所分成的组中包含的记录会越来越多。当步长gap减少为1时,整个数据会合成为一组,构成一组有序记录,则完成排序。
假设初始时有一个包含N个记录的无序序列
(1)在第一趟排序中,可以将步长gap1设置为gap1=N/2(取整),即相隔距离为gap1的记录组成一组,可以分为gap1组,接下来按照直接插入排序的方法分别对每组元素进行排序。
(2)在第二趟排序中,可以将步长gap2设置为gap2=gap1/2(取整),这样相隔距离为gap2的元素便可以组成一组,分为gap2组,接下来直接按照直接插入排序的方法分别对每组元素进行排序。
(3)以此类推,直到步长gap为1时,这时所有元素组成一组,对这组元素进行直接插入排序排序后,就可以完成排序了。
//希尔排序
public void shellSort(int[] arr)
{
int gap = arr.length / 2;//初始步长
//按照步长gap进行分组,对每组元素采用直接插入排序方法进行排序
while(gap >= 1)//gap等于1时所有元素组成一组
{
for(int i = gap; i < arr.length;i++)
{
int tmp = arr[i];
int j = 0;//定义并初始化局部变量
for(j = i - gap;j >= 0 && arr[j] > tmp;j = j - gap)//从后往前
{
arr[j + gap] = arr[j];
}
arr[j + gap] = tmp;
}
gap /= 2;//gap=agp/2取整
}
}
希尔排序的特点:
(1)最好情况下时间复杂度是O(N),平均时间复杂度为O(NlogN)。
(2)希尔排序是不稳定的排序算法。
直接插入排序和希尔排序的比较:
(1)直接插入排序是稳定的排序算法(因为在排序过程中不会改变相等的两个元素的相对位置),而希尔排序是不稳定的排序算法。
(2)直接插入排序更适合于原始序列中的元素基本有序的集合,原始序列越接近于正序,直接插入排序的性能越好(最好情况下的时间复杂度为O(N)),原始序列越接近于反序,直接插入排序的性能越差(最坏情况下的时间复杂度为O(N ^ 2)),平均时间复杂度为O(N ^ 2),空间复杂度为O(1)(因为只使用了一个临时变量来存储将要插入的元素的值)。
(3)希尔排序的比较次数和移动次数都比直接插入排序的要少,N越大时效果越明显,希尔排序算法的最好情况下时间复杂度为O(N),平均情况下时间复杂度为O(NlogN),空间复杂度为O(1)(因为只使用了一个临时变量来存储将要插入的元素的值)。
(4)在希尔排序中,增量gap的取值必须满足:最后一次排序的步长为1。
(5)直接插入排序适用于链式结构,希尔排序不适用于链式结构。
5.简单选择排序
算法思想:每一趟都从待排序的记录中选出关键字最小的记录,顺序放到已经排好序的记录序列的末尾,直到全部排序结束为止。
假设原始序列中一共有N个记录
(1)从待排序的序列中,找到关键字最小的元素。如果这个关键字最小的元素不是待排序序列的第一个元素,则交换该元素与第一个元素的位置;
(2)从余下的N-1个元素中,再找出一个关键字最小的元素,重复上述步骤,直到排序结束。
(3)简单选择排序的最小值率先出现在数组的头角标位上,在第i趟排序过程中,前面的i-1个元素已经排好序了,此时将当前第i小的元素放在位置i上。
//简单选择排序
public void selectSort(int[] arr)
{
for(int i = 0;i < arr.length - 1;i++)
{
int tmp = arr[i];
int index = i;
for(int j = i + 1;j < arr.length;j++)//从待排序的序列中找到关键字最小的元素
{
if(arr[j] < tmp)
{
tmp = arr[j];
index = j;
}
}
arr[index] = arr[i];
arr[i] = tmp;
}
}
简单选择排序的特点:
(1)简单选择排序是一种不稳定的排序算法。
(2)简单选择排序的比较次数与序列的初始排序无关,假设待排序的序列一共有N个元素,都需要比较(N-1)+(N-2)+…+1=(N-1+1) * (N-1)/2=N * (N-1)/2次;而简单选择排序的移动次数与序列的初始排序有关,在最好情况下即序列初始正序时,移动次数为0;最坏情况下即序列初始反序时,移动次数最多。所以简单排序的最好情况下、最坏情况下和平均情况下的时间复杂度均为O(N ^ 2)。
(3)简单选择排序的空间复杂度为O(1)(因为只使用了一个临时变量tmp来保存关键字最小的元素)。
6.堆排序
堆是一棵顺序存储的完全二叉树。
其中,每个节点的关键字都不大于(<=)其孩子节点的关键字,这样的堆称为小根堆;
每个节点的关键字都不小于(>=)其孩子节点的关键字,这样的堆称为大根堆。
对于n个元素的序列{R0,R1,…,Rn-1},当且仅当满足下列关系之一时,称之为堆:
R[i]<=R[2i+1]且R[i]<=R[2i+2](小根堆)
R[i]>=R[2i+1]且R[i]>=R[2i+2](大根堆)
其中i=0,1,…,(n-3)/2。
算法思想:
(1)构建堆,根据初始数组去构造初始堆,构建一个完全二叉树,保证所有父结点的值均不小于其左右孩子结点的值(大根堆)。
(2)交换堆顶元素和最后一个元素的位置,每次交换第一个元素和最后一个元素,输出最后一个元素(最大值),然后把剩下的元素在调整为一个大根堆。
(3)当输出完最后一个元素后,这个数组已经是按照从小到大的顺序排列了。
//堆排序(大顶堆:升序)
public void heapAdjust(int[] arr, int pos, int len)//大根堆(父结点的值>=其左右孩子结点的值)
{
int tmp = 0;
int child = 0;
for(tmp = arr[pos];2 * pos + 1 <= len;pos = child)
{
child = 2 * pos + 1;//父结点的左子节点
if(child + 1 <= len && arr[child] < arr[child + 1])
child++;
if(arr[child] < tmp)
break;//跳出循环
else
arr[pos] = arr[child];
}
arr[pos] = tmp;
}
public void heapSort(int[] arr)
{
//1.构造堆
for(int i = arr.length / 2 - 1;i >= 0;i--)
{
heapAdjust(arr, i, arr.length - 1);
}
for(int i = arr.length - 1; i > 0;i--)
{
//2.交换堆顶元素(堆顶元素必为最大值)和最后一个元素的位置
int tmp = arr[0];
arr[0] = arr[i];
arr[i] = tmp;
//去掉最后一个元素(交换后最后一个元素是最大值)后,再将剩余元素调整成一个大顶堆
heapAdjust(arr, 0, i - 1);
}
}
特点:
(1)堆排序的最好情况、平均情况和最坏情况的时间复杂度均为O(NlogN),空间复杂度为O(1)。
(2)堆排序是不稳定的排序算法。
7.归并排序
归并排序的算法思想:将待排序序列R[0,1,…,n-1]先看成是n个长度为1的有序序列,将相邻的有序表成对归并,得到n/2个长度为2的有序序列;将这些有序序列再次归并,得到n/4个长度为4的有序序列;入此反复进行下去,最后得到一个长度为n的有序序列。
综上可知:归并排序其实要做两件事情:
“分解”——将序列每次折半拆分
“合并”——将划分后的序列段两两排序后合并
//归并排序
//合并
public void merge(int[] arr, int low, int mid, int high)
{
int len1 = mid - low + 1;
int[] L = new int[len1];
int len2 = high - mid;
int[] R = new int[len2];
int i, j, k;
for(i = 0, k = low;i < len1;i++, k++)
{
L[i] = arr[k];
}
for(j = 0, k = mid + 1; j < len2; j++, k++)
{
R[j] = arr[k];
}
i = 0;
j = 0;
k = low;
while(i < len1 && j < len2)
{
if(L[i] <= R[j])//考虑到归并排序是稳定的排序算法,即在排序过程中不会改变两个相同元素的相对位置关系。
{
arr[k] = L[i];
i++;
k++;
}
else
{
arr[k] = R[j];
j++;
k++;
}
}
while(i < len1)
{
arr[k++] = L[i++];
}
while(j < len2)
{
arr[k++] = R[j++];
}
}
//拆分
public void mergeSort(int[] arr, int l, int r)
{
if(l < r)
{
int mid = (r + l) / 2;
mergeSort(arr, l, mid);
mergeSort(arr, mid + 1, r);
merge(arr, l, mid, r);
}
}
特点:
(1)归并排序的形式本质上是一棵二叉树,而且是一颗完全二叉树,因此它需要遍历的次数就是完全二叉树的深度,所以归并排序的最好情况、平均情况和最差情况的时间复杂度都是O(NlogN)。
(2)因为在算法过程中我们有定义临时的数组L和R来存放元素,所以空间复杂度为O(N)。
(3)归并排序法是稳定的排序方法。
归并排序法和堆排序法、快速排序法的比较:
(1)由于归并排序法形式上本质还是一棵二叉树,而且是一棵完全二叉树,其遍历的次数就是该完全二叉树的深度,因此归并排序法的最好情况、平均情况和最坏情况的时间复杂度都是O(NlogN);而堆排序法是一棵顺序存储的完全二叉树,因此堆排序法的最好情况、平均情况和最坏情况的时间复杂度都是O(NlogN);而快速排序法最好情况和平均情况的时间复杂度为O(NlogN),但是最坏情况的时间复杂度为O(N ^ 2)。因此,如果从时间复杂度的角度来看,首选归并排序和堆排序。
(2)由于在合并过程中我们定义了两个数组L和R来临时存放元素,因此归并排序法的空间复杂度为O(N);堆排序的空间复杂度为O(1);快速排序法由于是递归完成的,而递归需要将数据暂时存放在栈空间中,所以快速排序法的空间复杂度为O(logN)。因此,如果从空间复杂度上来看,首选堆排序,次选快速排序,最后选归并排序。
(3)由于归并排序法是稳定的排序方法,而堆排序和快速排序都是不稳定的排序方法。因此,如果从稳定性的角度来看,应选归并排序。
(4)若从平均情况的排序速度来看,应选快速排序(快速排序法的“快速”可不是浪得虚名~)。
8.基数排序
算法思想:
基数排序与上述7种排序算法都不一样,因为它并不需要比较关键字的大小,它是根据关键字中各个数位上的值,通过对排序的N个元素进行若干趟的“分配”和“收集”来实现排序的。
例如,我们现在有一个待排序的序列R{50,123,543,187,49,30,0,2,11,100},我们知道,任何一个阿拉伯数字,其各个数位上的值都是以基数0 ~ 9来表示的,因此我们可以将0 ~ 9视为10个编号分别为0 ~ 9的桶。
(1)首先根据各个元素的个位上的值将所有元素分别分配到这10个桶中,例如R[0]=50,它就被分配到编号为0的桶中,如下图所示。
(2)分类后,我们再从编号为0 ~ 9的这10个桶中依次将元素收集起来,便得到了一组按照个位数升序排序的序列R{50,30,0,100,11,2,123,543,187,49}的序列。
(3)可以继续按照十位数对这个序列中的元素进行分配,就得到了按照十位数升序排序的序列R{0,100,2,11,123,30,543,49,50,187}。
(4)最后按照百位数对这个序列中的元素进行分配,就得到了按照百位数升序排序的序列R{0,2,11,30,49,50,100,123,187,543},这样就完成了对原始序列的升序排序了。
特点:基数排序法是稳定的排序方法。