本文主要是介绍常见排序算法,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
常见内部排序算法
- 概述
- 插入类排序
-
- 交换类排序
-
- 选择类排序
-
- 归并排序
- 基数排序
概述
插入类排序
直接插入排序
public class SF{
public int[] sortArray(int[] nums) {
for(int i = 1;i < nums.length;i++){ //初始时nums[0]为有序序列,因此从1开始遍历待插入记录
int temp = nums[i]; //将待插入的元素存入临时变量temp中
int k = i-1; //从已有序的序列的最后一个元素开始向前顺序遍历,寻找插入位置
for(;k >= 0 && nums[k] > temp;k--){
nums[k+1] = nums[k]; //如果已有序序列中的元素k大于待插入元素,则将元素k往后移动一位
}
nums[k+1] = temp; //将待插入元素插入合适的位置
}
return nums;
}
}
- 直接插入排序是稳定的,即关键字相等的两个记录的顺序在排序前后不会有所改变
- 最好情况下(待排序记录已经有序),总的比较次数为nums.length-1次,移动记录的次数为2(nums.length-1)(每次只对待插记录nums[i]移动两次),因此最好时间复杂度为O(n)
- 最坏情况下(待排序记录为逆序)
总的比较次数
总的移动次数
因此最坏情况下总的时间复杂度为O(n²) - 当记录随机分布时,平均状况下的时间复杂度也为O(n²)
- 排序过程中用到的额外空间为temp变量,且与问题规模无关,因此空间复杂度为O(1)
折半插入排序
class SF {
public int[] sortArray(int[] nums) {
for(int i = 1;i < nums.length;i++){
int temp = nums[i];
int high = i-1;
int low = 0;
while(low <= high){ //通过二分查找法寻找插入位置,可将比较次数的数量级降低为O(nlogn)
int mid = low+(high-low)/2;
if(temp < nums[mid]){
high = mid-1;
}else{
low = mid+1;
}
}
for(int j = i-1;j >=low;j--){
nums[j+1] = nums[j];
}
nums[low] = temp;
}
return nums;
}
}
- 折半插入排序是稳定的
- 当记录随机分布(平均时间复杂度),折半插入排序在寻找插入位置时,采用二分查找法,可以将比较次数的数量级降低到O(nlogn),但是移动次数的数量级依然是O(n²),因此总的平均时间复杂度仍然为O(n²)
- 最好情况下(待排序记录有序),只需要对待插入记录移动2(nums.length-1)次,因此最好的时间复杂度为O(nlogn)
- 最坏情况下(待排序记录逆序),移动次数的数量级是O(n²),因此最坏的时间复杂度为O(n²)
- 空间复杂度为O(1)
希尔排序
class SF{
public int[] sortArray(int[] nums){
int delta = nums.length/2; //第一次选取的增量为nums.length的一半
while(delta >= 1){
for(int i = 0;i < delta;i++){ //同一个增量下要对形成的每一个序列进行直接插入排序
shell(nums,delta,i);
}
delta/=2; //增量每次减半,直至增量为1
}
return nums;
}
public void shell(int[] nums,int delta,int init){
for(int i = delta+init;i < nums.length;i+=delta){
int temp = nums[i];
int k = i - delta;
for(;k >= 0 && temp < nums[k];k -= delta) {
nums[k + delta] = nums[k];
}
nums[k + delta] = temp;
}
}
}
- 希尔排序将待排序序列划分成若干个较小的子序列,然后对子序列进行直接插入排序。当增量缩小为1时,待排序记录已基本有序,这时再进行直接插入排序时,序列已基本有序,此时直接插入排序的性能最佳,一般认为希尔排序的平均时间复杂度为O(n∧1.5),比直接对待排序序列使用直接插入排序的性能要好
- 希尔排序是不稳定的
交换类排序
冒泡排序
基本的冒泡排序,没有优化
class SF{
public int[] sortArray(int[] nums){
if(nums == null || nums.length == 0){
return null;
}
for(int i = 0;i < nums.length;i++){ //服务于内层循环
for(int j = 0;j < nums.length-i-1;j++){
if(nums[j] > nums[j+1]){
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
return nums;
}
}
- 基本的冒泡排序,不论序列的分布情况如何,时间复杂度都为O(n²)。额外空间为temp,且与问题规模无关,因此空间复杂度为O(1)
- 冒泡排序是稳定的
冒泡排序的优化
class SF {
public int[] sortArray(int[] nums){
if(nums == null || nums.length == 0){
return null;
}
for(int i = 0;i < nums.length;i++){
boolean flag = false;
for(int j = 0;j < nums.length-i-1;j++){
if(nums[j] > nums[j+1]){
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
flag = true;
}
}
if(flag == false){
break;
}
}
return nums;
}
}
- 如果某一轮中没有发生元素交换,则整个待排序序列已经有序,不需要再进行排序,直接跳出循环。
- 最好情况下(待排序序列一开始就有序),外循环只进行一次,内循环循环一轮,此时的时间复杂度为O(n)
冒泡排序的优化
class SF {
public int[] sortArray(int[] nums){
if(nums == null || nums.length == 0){
return null;
}
int lastExchangeIndex = 0;
int sortBorder = nums.length-1;
for(int i = 0;i < nums.length;i++){
boolean isSorted = false;
for(int j = 0;j < sortBorder;j++){
if(nums[j] > nums[j+1]){
int temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
isSorted = true;
lastExchangeIndex = j;
}
}
sortBorder = lastExchangeIndex;
if(isSorted == false){
break;
}
}
return nums;
}
}
- 在每一轮比较中,确定实际的有序序列区,这样可以避免在下一轮比较中进行无效比较,从而提高算法性能
快速排序
class SF{
public int[] sortArray(int[] nums){
quickSort(nums,0,nums.length-1);
return nums;
}
public void quickSort(int[] nums,int left,int right){
if(nums == null || right <= left){
return;
}
int mid = move(nums,left,right);
quickSort(nums,left,mid-1);
quickSort(nums,mid+1,right);
}
public int move(int[] nums,int left,int right){
int pivot = nums[left];
while(left < right){
while(left < right && nums[right] >= pivot){
right--;
}
nums[left] = nums[right];
while(left < right && nums[left] <= pivot){
left++;
}
nums[right] = nums[left];
}
nums[left] = pivot;
return left;
}
}
- 快速排序的本质是一颗二叉树的前序遍历,因此我们从树的角度来分析快速排序的时间复杂度,二叉树中的每一个节点(除了叶子节点)都是一个待排序序列,因此总的时间复杂度应为二叉树中每个节点(除了叶子节点)的长度(每个待排序序列的长度)加起来的总和
- 由于对序列的划分是由枢轴元素决定的,如果序列一开始是顺序或逆序的,那么每次划分之后枢轴元素位于序列的最左端或最右端,二叉树就会生长为一个类似于单链表的结构,这时候时间复杂度就会上升到O(n²)
- 快速排序是递归式算法,平均状况下递归栈的深度为logn,因此平均状况下的空间复杂度为O(logn);最坏情况下,即序列一开始是顺序或逆序的,递归栈的深度为n,因此最坏情况下的空间复杂度为O(n)
下面演示一下平均状况下的递归栈深度
- 快速排序是不稳定的
选择类排序
简单选择排序
class SF {
public int[] sortArray(int[] nums) {
if(nums == null || nums.length == 0){
return null;
}
for(int i = 0;i < nums.length-1;i++){
int min = i;
for(int j = i+1;j < nums.length;j++){
if(nums[j] < nums[min]){
min = j;
}
}
if(min != i){
int temp = nums[i];
nums[i] = nums[min];
nums[min] = temp;
}
}
return nums;
}
}
- 简单选择排序是不稳定的
- 简单选择排序的最好、最坏、平均复杂度均为O(n²)
- 额外空间为temp,且与问题规模无关,因此简单选择排序的空间复杂度为O(1)
堆排序
class SF{
public int[] sortArray(int[] nums){
heapSort(nums);
}
public void heapSort(int[] nums){
creatHeap(nums);
for(int i = nums.length-1;i >= 1;i--){
int temp = nums[0];
nums[0] = nums[i];
nums[i] = temp;
sift(nums,0,i-1);
}
}
public void creatHeap(int[] nums){ //大根堆的初建
for(int i = nums.length/2;i >= 0;i--){
sift(nums,i,nums.length-1);
}
}
public void sift(int[] nums,int k,int m){ //大根堆的重建
int t = nums[k];
int i = k;
int j = 2*i;
boolean finished = false;
while(j <= m && !finished){
if(j+1 <= m && nums[j] <= nums[j+1]){
j = j+1;
}
if(t >= nums[j]){
finished = true;
}else{
nums[i] = nums[j];
i = j;
j = 2*i;
}
}
nums[i] = t;
}
}
- 堆排序是不稳定的
- 即使最坏情况下,时间复杂度也为O(nlogn);空间复杂度为O(1)
归并排序
class SF{
public int[] sortArray(int[] nums){
int[] temp = new int[nums.length]; //O(n)的空间复杂度
mergeSort(nums,0,nums.length-1,temp);
return nums;
}
public void mergeSort(int[] nums,int left,int right,int[] temp){
if(nums == null || left >= right){
return;
}
int mid = left+(right-left)/2;
mergeSort(nums,left,mid,temp);
mergeSort(nums,mid+1,right,temp);
merge(nums,left,mid,right,temp);
}
public void merge(int[] nums,int left,int mid,int right,int[] temp){
int i = left;
int j = mid+1;
int t = 0;
while(i <=mid && j <= right){
if(nums[i] <= nums[j]){
temp[t++] = nums[i++];
}else{
temp[t++] = nums[j++];
}
}
while(i <= mid){
temp[t++] = nums[i++];
}
while(j <= right){
temp[t++] = nums[j++];
}
t = 0;
i = left;
while(i <= right){
nums[i++] = temp[t++];
}
}
}
- 归并排序算法的时间复杂度分析和快速排序算法的时间复杂度分析十分相似,但是归并排序算法对序列的每次划分都是等分的,因此不会像快速排序算法那样出现斜二叉树(类似于单链表结构),故归并排序算法的最好、最坏、平均时间复杂度都为O(nlogn),但是需要长度为nums.length的辅助空间,因此空间复杂度为O(n)
- 归并排序算法是稳定的
基数排序
- 基数排序是一种低位优先的排序法。排序时先按最低位的值对记录进行初步排序,在此基础上再按次低位的值进行进一步排序,以此类推,直至最高位
class SF{
public int[] sortArray(int[] nums){
//从数组中找到最大元素
int max = getMax(nums);
//由最大元素确定最大位数
int bits = (max+"").length();
for(int i = 0,n = 1;i < bits;i++,n*=10){
//建立10个桶(基数个桶),每个桶的最大长度为待排序数组的长度
int[][] bucket = new int[10][nums.length];
//用以记录每个桶中实际元素的个数,初始值均为0
int[] bucketElementsCount = new int[10];
for(int j = 0;j < nums.length;j++){
//获取n位上的值
int bitDigit = (nums[j]/n)%10;
//将nums[j]存放到序号为bitDigit这个桶中,并且将记录序号为bitDigit这个桶中实际元素个数的变量bucketElementsCount[bit]自增1
bucket[bitDigit][bucketElementsCount[bitDigit]] = nums[j];
bucketElementsCount[bitDigit]++;
}
int index = 0;
for(int k = 0;k < bucketElementsCount.length;k++){
//如果bucketElementsCount[k] != 0,说明序号为k的这个桶不是空的
if(bucketElementsCount[k] != 0){
//将桶中的元素分配到nums数组中
for(int m = 0;m < bucketElementsCount[k];m++){
nums[index] = bucket[k][m];
index++;
}
}
}
}
return nums;
}
int getMax(int[] nums){
int max = nums[0];
for(int i = 1;i < nums.length;i++){
if(max < nums[i]){
max = nums[i];
}
}
return max;
}
}
- 基数排序是稳定的
- 最好、最坏及平均复杂度都为O(d(n+k)),其中d为待排序数组中元素的最大位数,n为待排序数组的长度,k为基数
- 我们这里的算法实现的空间复杂度为O(nk + k)
这篇关于常见排序算法的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!