一、基本介绍
十种常见排序算法可以分为两大类:
比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
稳 定 性:如果a=b,且a原本在b前面,排序之后a仍然在b的前面则称该算法是稳定的,否则称该算法不稳定。
二、算法详解及实现
1.1冒泡排序:是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换 过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
1.2代码实现:
void bubblingSort(int arr[]){
if(arr == null || arr.length <= 1)
return;
int temp;
int length = arr.length;
//用来判断还需不需要再进行下一次循环
boolean isEnd;
for(int i = 0 ; i < length ; i++){
isEnd = false;
//因为每一次循环都会将数组中最大的一个数确定出来,所有j<length-1-i
for(int j = 0 ; j < length-1-i ; j++){
if(arr[j+1] < arr[j]){
temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
isEnd = true;
}
}
//如果此次循环没有发生一次两两交换就退出
if(!isEnd)
break;
}
}
1.3疑问:大家有没有想过为什么冒泡排序最好的时候时间复杂度会是O(n)?
现在假设传进来的数组是已经从小到大排好序的,那么上面的冒泡排序算法,在i=0时进入下一个for循环中,但此次没有发生任意的两两交换所以直接退出,因此最好的时间复杂度时O(n)。
2.1选择排序:选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素, 存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
2.1代码实现:
void insertSort(int arr[]){
if(arr == null || arr.length <= 1)
return;
int length = arr.length;
//用来记录此次循环的最小值,temp用来交换
int minIndex,temp;
for(int i = 0 ; i < length-1 ; i++){
minIndex = i;
for(int j = i ; j < length ; j++){
if(arr[j]<arr[minIndex]){
minIndex = j;
}
}
temp = arr[minIndex];
arr[minIndex] = arr[i];
arr[i] = temp;
}
}
2.3选择排序无论传进来的数组顺序如何时间复杂度都时O(n*n)
3.1:插入排序:插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未 排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。
3.2代码实现
void insertSort(int[] arr){
if(arr == null || arr.length <= 1)
return;
//current用来记录当前元素
int current,temp;
for(int i = 1 ; i < arr.length ; i++){
temp = i-1;
current = arr[i];
//寻找arr[i]适合插入的位置
while(temp >= 0 && current < arr[temp]){
//将temp后移,第一次进来时temp+1就是current
arr[temp+1] = arr[temp];
temp--;
}
arr[temp+1] = current;
}
}
4.1希尔排序:1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会 优先比较距离较远的元素。希尔排序又叫缩小增量排序。
4.2算法流程简介:先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序
选择一个增量序列t1,t2,…按增量序列个数n,对序列进行n 趟排序
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序
仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
4.3代码实现
void shellSort(int arr[]){
if(arr == null || arr.length <= 1)
return;
//这里增量序列为length/2,即length/2,length/4,length/8...
//i代表希尔排序中n/2+1个元素或n/4+1....
//increment是增量序列
int i,j,temp,increment;
for(increment=arr.length/2 ; increment >= 1 ; increment = increment/2){
for(i = increment;i<arr.length ; i++){
temp = arr[i];
j = i - increment;
while (j>=0 && temp < arr[j]){
//将元素arr[j]放到arr[j+increment]
//例如第一次进入while循环,此时j+increment就是temp,即将j赋值给temp处
//第二次进入则会把此时的j赋值给前一个j处
arr[j+increment] = arr[j];
j-=increment;
}
arr[j+increment] = temp;
}
}
}
4.4 算法分析
希尔排序的核心在”增量“序列的设定。在我们的代码中选择的是increment = increment/2;的方式,可究竟应该选择什么样的增量才是最好的,目前还是一个数学难题,迄今为止还没有人能找到一种最好的增量序列。不管怎么说,希尔排序算法是第一个突破了O(n2)算法的时间复杂度。
5.1归并排序:归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典 型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两 个有序表合并成一个有序表,称为2-路归并。
void mergeSort(int arr[]){
////在排序前,先建好一个长度等于原数组长度的临时数组,避免递归中频繁开辟空间
int[] temp = new int[arr.length];
mergeSort(arr,0,arr.length-1,temp);
}
void mergeSort(int arr[],int left,int right,int temp[]){
if(left<right){
int mid = (left + right)/2;
mergeSort(arr,left,mid,temp);
mergeSort(arr,mid+1,right,temp);
mergeSort(arr,left,mid,right,temp);
}
}
void mergeSort(int[] arr, int left, int mid, int right,int temp[]) {
//左序列指针
int i = left;
//右序列指针
int j = mid+1;
//临时数组指针
int k = 0;
while(i<=mid && j<=right){
if(arr[i]<=arr[j]){
temp[k++] = arr[i++];
}else{
temp[k++] = arr[j++];
}
}
//将左边剩余元素填充进temp中
while(i<=mid){
temp[k++] = arr[i++];
}
//将右序列剩余元素填充进temp中
while(j<=right){
temp[k++] = arr[j++];
}
k=0;
//将temp中的元素全部拷贝到原数组中
while (left <= right) {
arr[left++] = temp[k++];
}
}
5.2算法分析:归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
6.1:快速排序:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这 两部分记录继续进行排序,以达到整个序列有序。
从数列中挑出一个元素,称为 “基准”(pivot);
重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到 任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
void quickSort(int arr[]){
quickSort(arr,0,arr.length-1);
}
void quickSort(int arr[],int left,int right){
if(left > right)
return;
int i = left;
int j = right;
//index为此次循环的基准数
int index = arr[i];
while(i != j){
while (arr[j] >= index && i < j){
j--;
}
while (arr[i] <= index && i < j){
i++;
}
if(i<j){
//交换两个数在数组中的位置
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
//最终将基准数归位
arr[left] = arr[i];
arr[i] = index;
//继续处理左边的,这里是一个递归的过程
quickSort(arr,left,i-1);
//继续处理右边的,这里是一个递归的过程
quickSort(arr,i+1,right);
}
7.1:堆排序:堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均 为O(nlogn),它也是不稳定排序。首先简单了解下堆结构。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。如下图:
同时,我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
该数组从逻辑上讲就是一个堆结构,我们用简单的公式来描述一下堆的定义就是:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序步骤一:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。
步骤二: 将堆顶元素与末尾元素进行交换,使末尾元素最大。然后继续调整堆,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
void heapSort(int arr[]){
int temp;
//构建最大堆
for(int i = arr.length/2-1 ; i >= 0;i--){
//从第一个非叶子结点从下至上,从右至左调整结构
adjustHeap(arr,i,arr.length);
}
//调正堆结构,交换堆顶元素和末尾元素
for(int i = arr.length-1;i > 0;i--){
temp = arr[i];
arr[i] = arr[0];
arr[0] = temp;
adjustHeap(arr,0,i);
}
}
void adjustHeap(int arr[],int i,int length){
int temp = arr[i];
//从i结点的左子结点开始,也就是2i+1处开始
for(int k=i*2+1;k<length;k=k*2+1){
//如果存在右子节点且左子结点小于右子结点,k指向右子结点
if(k+1<length && arr[k] < arr[k+1]){
k++;
}
//如果子节点大于父节点,将子节点值赋给父节点(不用进行交换)
if(arr[k] > temp){
arr[i] = arr[k];
i = k;
}else{
break;
}
}
//将temp值放到最终的位置
arr[i] = temp;
}
堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆两部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)...1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。