一、算法分类。
(1) 按照时间复杂度分类
时间复杂度:指执行算法所需要的计算工作量,就是需要运算的次数。
- 平方阶:直接插入排序,直接选择排序,冒泡排序
- 线性对数阶:快速排序,堆排序,归并排序
- 线性阶:基数排序等(这篇文章不做描述)
(2)按照排序实现方式分类
- 插入排序:直接插入,希尔排序(也叫缩小增量排序)
- 选择排序:直接选择排序,堆排序
- 交换排序:冒泡排序,快速排序
- 分治思想排序:快速排序,归并排序
(3)按照算法的稳定性分类
稳定性概念:
若待排序的序列中,存在多个具有相同关键字的记录,经过排序, 这些记录的相对次序保持不变,则称该算法是稳定的;若经排序后,记录的相对 次序发生了改变,则称该算法是不稳定的。
- 稳定算法:直接插入排序,冒泡排序,归并排序
- 不稳定算法:直接选择排序,希尔排序,堆排序,快速排序
二、算法优劣点及其适用场景分析
(1)直接插入排序,直接选择排序,冒泡排序。
- 直接插入排序,算法复杂度为O(n^2).由于算法复杂度比较高,通常适用于数据量比较小的业务场景,在对数据量小的且数据基本有序的场景下,复杂度可以降低为O(n),从而提高效率,且实现比较简单。这个排序是个稳定的排序算法。
- 直接选择排序,算法复杂度为O(n^2),同样复杂度较高,适用于数据量比较小的业务场景,且算法不稳定。
- 冒泡排序,是个比较传统的排序,算法复杂度在O(n^2),同样适用于小业务场景,当原表有序或基本有序时,冒泡排序将大大减少比较次数和移动记录的次数,时间复杂度可降至O(n).同样也是个稳定的算法
(2)希尔排序
希尔排序是对直接插入排序的一种高效改进,从而将算法的复杂度降低,将直接插入排序的单个插入排序方式,改为分组多个插入,从而提高排序效率,降低运算量。其中希尔排序的算法复杂度在O(n^1.3~n^2)之间。适用于数据量比较适中的情况。
(3)堆排序、快速排序、归并排序
- 堆排序,是对选择排序的改进,实现方式是通过建立小顶堆或者是大顶堆,然后将最值和数组最后一个进行交换,这样最后一个就是最值,倒数第二个就是第二大(小)的值,按照这样的方式递归处理剩下的数据,数据就排好序了。算法复杂度在O(nlogn).程序运行时间比较短,且空间复杂度为O(1),不怎么占用内存,算法不稳定,适用于数据量比较大且不太要求稳定性的场景。
- 快速排序,可以说是冒泡排序的改进,采用分而治之的思想,首先选取一个pivot,通常选用第一个值或者是最后一个作为pivot,然后将数据按照pivot值进行分割,将大于pivot的值放在pivot的右边,小于piv的值放在pivot的左边,这样就确定了一个位置,就是pivot在整个数据中的位置。然后将pivot的左边的数据和右边的数据都进行快速排序,这样递归运行,直到所有的位置都确定了,这样的话,整个数据就已经排好序了,这个排序是一个不稳定的原地排序算法,算法复杂度在O(nlogn)~O(n^2),通常算法复杂度在O(nlogn),在极少数情况下,比如对基本有序的数据进行排序的场景下算法将退化为冒泡排序,复杂度上升为O(n ^ 2), 适用于数据量比较大的且不要求稳定性的场景,对随机分布的数据,使用快速排序效果最佳。
- 归并排序,是一个稳定的算法,其中算法复杂度在O(nlogn),运行时间比较段,但空间复杂度在O(n),说白了就是耗内存,该算法采用分治的思想,就是将大的数据切分成小块,一直细分下去,然后对细分的最小块进行排序,然后对小块进行合并排序成大块,递归运行,直到递归合并到最终的一块,整个数据就排好序了。该算法适用于数据量大且要求稳定性的,但是比较占用内存
三、算法的简单实现
- 冒泡排序。
public static void BubbleSort(int arr[]) {
/*
* 冒泡排序。牺牲时间复杂度来换取稳定性.算法时间复杂度为O(N^2).这个是最简单的排序方式。第一次执行n-1次比较。第二次n-2比较。以此类推......
* 方法过于传统,适用于数据量比较小的场景。
*/
for (int i=0;i<arr.length;i++) {
for(int j=0;j<arr.length-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 SelectionSort(int [] arr) {
/*
* 选择排序。选择排序的复杂度为O(n^2).思想 :从其中找出最值,然后和第一个位置进行比较,如果第一个值就是最值,不进行任何操作。否则,将最值和第一个进行交换。
* 第一次循环保证了第一个值一定是最值,依次类推,第二次循环,则是第二大的值。......知道循环结束为止。
*/
for (int i=0;i<arr.length;i++) {
int max_index=i; //最值索引赋值。
for(int j=i;j<arr.length;j++) {
if(arr[max_index]<arr[j]) {
max_index=j; //得到最大值的下标。
}
}
if(max_index!=i) { //如果最值不等于当前的下标,则对这数组里面的两个值进行交换。
int temp=arr[i];
arr[i]=arr[max_index];
arr[max_index]=temp;
}
}
}
- 直接插入排序(降序)
public static void InsertSort(int a[]) {
/*
* 直接插入排序。思想:将无序序列插入有序序列中。算法复杂度为O(N^2).插入排序在运行过程中使得一部分是有序的。
* 这个排序是个稳定排序算法。其中对于基本有序的数据,算法复杂度可以降为O(n).适用于数据量比较小的场景,如果数据基本有序且要求稳定,则
* 选择插入排序.
*/
int key,i;
for(int k=1;k<a.length;k++) { //从第二个元素进行遍历排序,默认第一个数字为有序序列。
key=a[k];
i=k-1;
while(i>=0 && a[i]<=key){ //如果key大于前面的值,且i>=0;则将前面的值放置到后面一个位置。
a[i+1]=a[i];
i-=1; //每执行一次i自减一次。直到不满足条件为止。这样则保证前面的值都比当前的key大.后面的值都比当前的key值小。
}
a[i+1]=key;
}
}
- 希尔排序
public static void ShellSort(int arr[]) {
/*
* 希尔排序,在插入排序上的一次改进。现将整个序列划分成几个子序列,对其进行插入排序,然后在对整体进行插入排序。
* 希尔排序,又叫缩小增量排序。也是一个非稳定的排序算法。时间复杂度为O(n^(1.3—2))空间复杂度为O(1).
* 由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,
* 但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以shell排序是不稳定的。
* 希尔排序时间复杂度的下界是n*log2n。希尔排序没有快速排序算法快 O(n(logn)).比较适合中等规模。
* 希尔排序的时间复杂度与增量序列的选取有关
* 例如希尔增量时间复杂度为O(n²),而Hibbard增量的希尔排序的时间复杂度为O(n^1.5)
*/
int increasement = arr.length;
int i, j, k;
do
{
// 确定分组的增量
increasement = increasement / 3 + 1; //划分成多个组。
for (i = 0; i < increasement; i++)//每个组都需要进行排序,对组进行循环。
{
for (j = i + increasement; j < arr.length; j += increasement) //每一组需要排序的次数。
{
/*
* 组内插入排序。
*/
int temp = arr[j];
for (k = j - increasement; k >= 0 && temp < arr[k]; k -= increasement)
{
arr[k + increasement] = arr[k]; //将比temp大的数字都往后移动。直到temp不在小于arr[k]里面的值。
}
arr[k + increasement] = temp;
}
}
} while (increasement > 1); //当组为1的时候排序结束。
}
- 堆排序
public static void adjust(int arr[], int len, int index)
{
/*
* 堆排序是一种选择排序,其时间复杂度为O(nlogn)。其中这里分为小顶堆和大顶推。
* 堆排序的时间复杂度为O(n*log(n)), 非稳定排序,原地排序(空间复杂度O(1))。
* 当数组中有相等元素时,堆排序算法对这些元素的处理方法不止一种,故是不稳定的。
* 堆排序大部分场景下没有快速排序快,同时占用的内存比快速排序大,但是相对快速比较稳定。
*/
int left = 2*index + 1; //左节点。
int right = 2*index + 2; //右节点.
int maxIdx = index;
if(left<len && arr[left] > arr[maxIdx]) {maxIdx = left;}
if(right<len && arr[right] > arr[maxIdx]) {maxIdx = right;} // maxIdx是3个数中最大数的下标
if(maxIdx != index) // 如果maxIdx的值有更新,则对这其中的节点的值进行交换。
{ int tmp=arr[maxIdx];
arr[maxIdx]=arr[index];
arr[index]=tmp;
adjust(arr, len, maxIdx); // 往下递归调整其他不满足堆性质的部分
}
}
public static void HeapSort(int A[], int n)
{
//先建立大顶堆,初始化操作,首先从最后一个非叶子节点开始。时间复杂度为O(n).
for(int i=n/2-1; i>=0; i--)
{
adjust(A,n,i);
}//初始化完成后会在第一给位置生成最值。
//进行排序 时间复杂度为O(nlogN).
for(int i=n-1; i>=1; i--)
{
//最后一个元素和第一元素进行交换
int temp=A[i];
A[i] = A[0];
A[0] = temp;
//然后将剩下的无序元素继续调整为大顶堆
adjust(A,i,0);
}
}
- 快速排序
public static void Quick_Sort(float data[], int left, int right)
{
/*快速排序。这个是目前应用最广泛的排序方法。高效率但是不稳定。算法复杂度在O(nlogn).适合数据量比较大的场合。
* 这个对于无序的数组的排序非常适用。对于有序或者基本有序的数组进行排序则会退化到O(n^2)复杂度。
* 填坑挖坑法。这里面采用最后一个为pivot。方法实现过程相对比较简单。
*
*/
if (left < right)
{
int i = left, j = right;
float pivot = data[j];
while(i < j)
{
while(i < j && data[i] <= pivot) // 从左往右找到第一个比 pivot 大的数
{
i++;
}
if(i < j)
{
data[j--] = data[i];
}
while(i < j && data[j] >= pivot) // 从右往左找到第一个比 pivot 小的数
{
j--;
}
if(i < j)
{
data[i++] = data[j];
}
}
data[i] = pivot; // i=j
Quick_Sort(data, left, i-1);
Quick_Sort(data, i+1, right);
}
}
- 归并排序
public static void MergeSort(float sort_array[],int left,int right) {
/*
* 归并排序的递归实现。算法的时间复杂度(O(nlogN)),空间复杂度为(O(N)).属于稳定的排序方法,但不是原地排序方法。
* 这个方法缺点就是需要大量的内存空间。O(n).
* 适用于数据量较大的场景,且要求稳定性的场景。
*/
if(left<right) {
int mid=(left+right)/2;
MergeSort(sort_array,left,mid);
MergeSort(sort_array,mid+1,right);
Merge_Array(sort_array,left,right);
}
}
private static void Merge_Array(float[] sort_array, int left, int right) {
int mid=(left+right)/2;
int i=left;
int j=mid+1;
int k=0;
float temp[]=new float [right-left+1];
while(i<=mid && j<=right) {
if(sort_array[i]>=sort_array[j]) {
temp[k++]=sort_array[i++];
}
else {
temp[k++]=sort_array[j++];
}
}
while(i<=mid) {temp[k++]=sort_array[i++];}
while(j<=right) {temp[k++]=sort_array[j++];}
for (int f=0;f<k;f++) {
sort_array[left++]=temp[f];
}
}