经典排序算法(JAVA)
对于数据结构算法来说,排序算法虽然很基本,却很重要,如何在合适的场景选择合适的排序方式呢?这就需要了解每种排序的特点了。
冒泡排序(Bubble Sort)
冒泡排序是内部排序(空间复杂度O(1),复制原数组是为了排序时不改变原数组数据,不算在辅助空间内),是一种稳定的排序算法。
最好情况时间复杂度为O(n),最坏情况为 O(n²),平均时间复杂度为O(n²)。
步骤:
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
public int[] bubbleSort(int[] sourceArray) {
//复制原数组,为了排序时不改变原数组数据,不算在辅助空间内
int[] resArray= Arrays.copyOf(sourceArray, sourceArray.length);
for(int i=1;i<resArray.length;++i){
//每次循环开始设定一个标记flag,当值为真时,说明本次循环没有发生交换,即数组已有序,直接可以跳出循环(是对最基本冒泡排序的优化,但是对平均时间复杂度没有影响)
boolean flag=true;
for(int j=0;j<resArray.length-i;++j){
if(resArray[j]>resArray[j+1]){
int temp=resArray[j];
resArray[j]=resArray[j+1];
resArray[j+1]=temp;
flag=false;
}
}
if(flag)break;
}
return resArray;
}
选择排序(Selection Sort)
选择排序是内部排序(空间复杂度O(1)),是一种不稳定的排序算法。
最好情况和最坏情况时间复杂度都为 O(n²),平均时间复杂度为O(n²)。
步骤:
- 第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,
- 然后再从剩余的未排序元素中寻找到最小(大)元素,然后交换到已排序的序列的末尾。
- 循环这个过程,直到全部待排序的数据元素的个数为零。
public int[] selectionSort(int[] sourceArray) {
int[] resArray= Arrays.copyOf(sourceArray, sourceArray.length);
for(int i=0;i<resArray.length-1;++i){
int min=0;
for(int j=i+1;j<resArray.length;++j){
if(resArray[j]<resArray[min]) min=j;
}
if(min!=i){
int temp=resArray[i];
resArray[i]=resArray[min];
resArray[min]=temp;
}
}
return resArray;
}
插入排序(Insertion Sort)
插入排序是内部排序(空间复杂度O(1)),是一种稳定的排序算法。
最好情况时间复杂度为O(n),最坏情况为 O(n²),平均时间复杂度为O(n²)。
步骤:
- 从第一个元素开始,该元素可以认为已经被排序
- 取出下一个元素,在已经排序的元素序列中从后向前扫描
- 如果该元素(已排序)大于新元素,将该元素移到下一位置,直到找到已排序的元素小于或者等于新元素的位置,将新元素插入到下一位置中
public int[] insertionSort(int[] sourceArray) {
int[] resArray= Arrays.copyOf(sourceArray, sourceArray.length);
for(int i=1;i<resArray.length;++i){
//待排序元素存到temp
int temp=resArray[i];
int j=i;
while (j>0 && tmp<resArray[j-1]) {
resArray[j]=resArray[j-1];
j--;
}
resArray[j]=temp;
}
return resArray;
}
希尔排序(Shell Sort)
希尔排序是内部排序(空间复杂度O(1)),是一种不稳定的排序算法。
最好情况时间复杂度为O(nlog²n),最坏情况为 O(nlog²n),平均时间复杂度为O(nlogn)。
希尔排序是基于插入排序的一种算法,先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录"基本有序"时,再对全体记录进行依次直接插入排序。
步骤:
- 选择一个增量序列
通常先取一个小于数组长度的整数d1作为第一个增量,把文件的全部记录分组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量为1,即所有记录放在同一组中进行直接插入排序为止。
- 按增量序列个数 k,对序列进行 k 趟排序;
- 每趟排序,根据对应的增量 ,将待排序列分割成若干子序列,分别对各子表进行直接插入排序。仅增量因子为 1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。
public int[] shellSort(int[] sourceArray) {
int[] resArray= Arrays.copyOf(sourceArray, sourceArray.length);
int gap=resArray.length;
//这里选择的增量序列第一个值为数组长度减半,接下来每次循环都减半直到为1
while(gap>0){
gap/=2;
for(int i=gap;i<resArray.length;++i){
int temp=resArray[i];
int j=i-gap;
while(j>=0 && resArray[j]>temp){
resArray[j+gap]=resArray[j];
j-=gap;
}
resArray[j+gap]=temp;
}
}
return resArray;
}
归并排序(Merge Sort)
归并排序是外部排序(空间复杂度O(n)),是一种稳定的排序算法。
最好情况时间复杂度为O(nlogn),最坏情况为 O(nlogn),平均时间复杂度为O(nlogn)。归并排序是分治算法的一个非常典型的应用。
步骤:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置;
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复上一步直到某一指针超出序列尾;
- 将另一序列剩下的所有元素直接复制到合并序列尾。
public int[] mergeSort(int[] sourceArray) {
if(sourceArray.length<2)return sourceArray;
int[] resArray= Arrays.copyOf(sourceArray, sourceArray.length);
int middle = resArray.length / 2;
int[] left = Arrays.copyOfRange(resArray, 0, middle);
int[] right = Arrays.copyOfRange(resArray, middle, resArray.length);
//递归
return merge(mergeSort(left), mergeSort(right));
}
protected int[] merge(int[] left, int[] right) {
int[] res = new int[left.length + right.length];
int i = 0;
while (left.length > 0 && right.length > 0) {
if (left[0] <= right[0]) {
res[i++] = left[0];
//去掉数组的首位
left = Arrays.copyOfRange(left, 1, left.length);
} else {
res[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
}
//某一指针超出序列尾,将另一序列剩下的所有元素直接复制到合并序列尾
while (left.length > 0) {
result[i++] = left[0];
left = Arrays.copyOfRange(left, 1, left.length);
}
while (right.length > 0) {
result[i++] = right[0];
right = Arrays.copyOfRange(right, 1, right.length);
}
}
快速排序(Quick Sort)
快速排序是内部排序(空间复杂度O(logn)),是一种不稳定的排序算法。
最好情况时间复杂度为O(nlogn),最坏情况为 O(n²),平均时间复杂度为O(n²)。
快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。快速排序又是一种分而治之思想在排序算法上的典型应用。
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。
步骤:
- 设置两个变量i、j,排序开始的时候:i=0,j=N-1;
- 以第一个数组元素作为关键数据,赋值给key,即key=A[0];
- 从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]的值交换;
- 从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换;
- 重复上述过程,直到i=j; (没找到符合条件的值,即A[j]不小于key,A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。
public int[] quickSort(int[] sourceArray,int left,int right) {
if(sourceArray.length<2)return sourceArray;
int[] resArray= Arrays.copyOf(sourceArray, sourceArray.length);
int pivot = arr[left];
int i = left;
int j = right;
while (i<j) {
while ((i<j)&&(resArray[j]>pivot)) {
j--;
}
while ((i<j)&&(resArray[i]<pivot)) {
i++;
}
if ((resArray[i]==resArray[j])&&(i<j)) {
i++;
} else {
int temp = resArray[i];
resArray[i] = resArray[j];
resArray[j] = temp;
}
}
if (i-1>left) resArray=quickSort(resArray,left,i-1);
if (j+1<right) resArray=quickSort(resArray,j+1,right);
return resArray;
}
堆排序(Heap Sort)
堆排序是内部排序(空间复杂度O(1)),是一种不稳定的排序算法。
最好情况时间复杂度为O(nlogn),最坏情况为 O(nlogn),平均时间复杂度为O(nlogn)。
堆是一个近似完全二叉树的结构, 堆中子结点的键值或索引总是小于(或者大于)它的父节点。
步骤:
- 创建一个堆(大根堆或者小根堆);
- 把堆首(最大值)和堆尾互换;
- 把堆的尺寸缩小 1,并把新的数组顶端数据调整到相应位置;
- 重复上述步骤 ,直到堆的尺寸为 1。
public int[] heapSort(int[] sourceArray,int left,int right) {
if(sourceArray.length<2)return sourceArray;
int[] resArray= Arrays.copyOf(sourceArray, sourceArray.length);
//建堆
for (int i = resArray.length / 2 - 1; i >= 0; i--) {
adjustHeap(resArray, i, resArray.length);
}
//排序
for (int j = resArray.length - 1; j > 0; j--) {
swap(resArray, 0, j);
adjustHeap(resArray, 0, j);
}
rerurn resArray;
}
//堆的调整
public static void adjustHeap(int[] array, int i, int length) {
int temp = array[i];
for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
if (k + 1 < length && array[k] < array[k + 1]) {
k++;
}
if (array[k] > temp) {
swap(array, i, k);
i = k;
} else {
break;
}
}
}
//交换
public static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
堆排序还学习过左神的方法,真的很清晰:
public static void heapSort(int[] arr) {
if (arr == null || arr.length < 2) {
return;
}
for (int i = 0; i < arr.length; i++) {
heapInsert(arr, i);
}
int size = arr.length;
swap(arr, 0, --size);
while (size > 0) {
heapify(arr, 0, size);
swap(arr, 0, --size);
}
}
public static void heapInsert(int[] arr, int index) {
while (arr[index] > arr[(index - 1) / 2]) {
swap(arr, index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
public static void heapify(int[] arr, int index, int size) {
int left = index * 2 + 1;
while (left < size) {
int largest = left + 1 < size && arr[left + 1] > arr[left] ? left + 1 : left;
largest = arr[largest] > arr[index] ? largest : index;
if (largest == index) {
break;
}
swap(arr, largest, index);
index = largest;
left = index * 2 + 1;
}
}
public static void swap(int[] arr, int i, int j) {
int tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
计数排序(Counting Sort)
计数排序是外部排序(空间复杂度O(k),k是整数的范围),是一种稳定的排序算法。最好情况时间复杂度为O(n+k),最坏情况为O(n+k),平均时间复杂度为O(n+k)。
步骤:
- 找出待排序的数组中最大和最小的元素;
- 统计数组中每个元素 x 出现的次数,存入数组count的第 x 项;
- 对所有的计数累加(从数组count中的第一个元素开始,每一项和前一项相加)
- 反向填充目标数组:将每个元素 x 放在数组count的第 x 项,每放一个元素就将count[x] 减去1
public int[] countingSort(int[] sourceArray) {
int[] resArray= Arrays.copyOf(sourceArray, sourceArray.length);
int[] resArray= new int[sourceArray.length];
int max = sourceArray[0],min = sourceArray[0];
for(int i:sourceArray){
if(i>max){
max=i;
}
if(i<min){
min=i;
}
}
int k=max-min+1;
int help[]=new int[k];
for(int i=0;i<sourceArray.length;++i){
help[sourceArray[i]-min]+=1;
}
for(int i=1;i<help.length;++i){
help[i]=help[i]+help[i-1];
}
for(int i=sourceArray.length-1;i>=0;--i){
resArray[--help[sourceArray[i]-min]]=sourceArray[i];
}
return resArray;
}