十大排序 JAVA&&GO语言实现版本

80 阅读12分钟

十大排序

概念

本文说的排序方法都属于内部排序,即数据量小,足以在内存空间完成排序。

分类:

  • 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破 O(nlogn),因此也称为非线性时间比较类排序。
  • 非比较排序:不通过比较来决定元素间的相对次序,而是通过确定每个元素之前,应该有多少个元素来排序。可以达到O(n)时间复杂度,因此称为线性时间非比较类排序。

1. 冒泡排序

将大的数值向下沉,轻的数值向上浮。

步骤

for i in (0...n-1){
    for j in (0...n-i-1){
        比较相邻两个数的大小,如果左边的数值大于右边,交换;
    }
    完成一轮循环后,n-i-1及其以后的数值位置已经确定;
}

GO语言代码

func BubbleSort(ints []int) []int{
    flag:=true
    for i:=0;i<len(ints);i++{
        for j:=0;j<len(ints)-i-1;j++{
            if ints[j]>ints[j+1]{
                ints[j],ints[j+1]=ints[j+1],ints[j]
                flag=false
            }
        }
        if flag{
            break
        }
    }
    return ints
}

JAVA版本

public static int[] Bubblesort(int[] arr) {
    boolean flag=true;
    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;
                flag=false;
            }
        }
        if (flag){
            break;
        }
    }
    return arr;
}

也是直接传递引用

分析

  • 稳定性:设置的是小于才交换,是稳定的。如果小于等于,那会导致不稳定。
  • 时间复杂度:最好的情况是已经排好序的,flag为true,那么时间复杂度为O(1)。平均情况是两层循环O(n^2)
  • 空间复杂度:O(1)

2. 选择排序

遍历得到没排好序的数组中最小的,放在当前没排好序的数组的最前面

遍历n次,0-n-1排好

步骤

for i in (0...n-1){
    int min_value
    for j in (i...n-1){
        更新min_value所在下标
    }
    将min_value交换到到第i的位置,此时0-i已排好
}

Go版本

func SelectSort(nums []int) []int{
    for i:=0;i<len(nums);i++{
        minVal:=i
        for j:=i;j<len(nums);j++{
            if nums[j]<nums[minVal]{
                minVal=j
            }
        }
        nums[i],nums[minVal]=nums[minVal],nums[i]
    }
    return nums
}

java

public static int[] SelectSort(int[] arr){
    for (int i=0;i<arr.length;i++){
        int minVal=i;
        for (int j=i;j<arr.length;j++){
            if (arr[j]<arr[minVal]){
                minVal=j;
            }
        }
        swap(arr,i,minVal);
    }
    return arr;
}

分析

  • 稳定性:不稳定
  • 时间:O(n^2)
  • 空间:O(1)

3. 插入排序

步骤

for i in (1...n-1){  //默认第0个是排好序的序列
    cur=nums[i]
    pre_index=i-1
    //从后向前,每一个数值和cur对比,比cur大就向后移动
    while ( pre_index>=0 && cur<nums[pre_index] ){
        nums[pre_index+1]=nums[pre_index]
        pre_index--
    }
    nums[pre_index+1]=cur
    //至此 0..i都是排好序的
}
// 最后0..n-1都是排好序的

GO

func InsertSort(nums []int) []int{
    for i:=1;i<len(nums);i++{
        cur:=nums[i]
        preIndex:=i-1
        for preIndex>=0 && nums[preIndex]>cur{
            nums[preIndex+1]=nums[preIndex]
            preIndex--
        }
        nums[preIndex+1]=cur
    }
    return nums
}

JAVA

public static int[] InsertSort(int[] arr){
    for (int i=1;i<arr.length;i++){
        int cur=arr[i],preIndex=i-1;
        while (preIndex>=0&&arr[preIndex]>cur){
            arr[preIndex+1]=arr[preIndex];
            preIndex--;
        }
        arr[preIndex+1]=cur;
    }
    return arr;
}

分析

  • 稳定性:稳定,大于才向后挪,等于不变
  • 时间:好的情况下,本来就顺序排列,遍历一遍完成O(n),正常O(n^2)
  • 空间:O(1)
  • 注意:preIndex>=0&&arr[preIndex]>cur顺序不能变 需要先判断preIndex的范围

4. 希尔排序

希尔排序有不同的增量序列,排序的时间复杂度也和增量序列有关。

一般的希尔排序都是使用希尔增量序列(n/2,n/4....)

使用希尔增量进行分组后,对每个分组进行插入排序

步骤

gap=n/2
for gap>0{
	// 插入排序
	for i in (gap...n-1){
		preIndex=i-gap
		cur=nums[i]
		for preIndex>=0 && nums[preIndex]>cur{
			nums[preIndex+gap]=nums[preInde]
			preIndex=preIndex-gap
		}
		nums[preIndex+gap]=cur
	}
	gap=gap/2
}

GO

func ShellSort(nums []int) []int{
	gap:=len(nums)/2
	for gap>0{
		for i:=gap;i<len(nums);i++{
			cur:=nums[i]
			preIndex:=i-gap
			for preIndex>=0 && nums[preIndex]>cur{
				nums[preIndex+gap]=nums[preIndex]
				preIndex=preIndex-gap
			}
			nums[preIndex+gap]=cur
		}
		gap=gap/2
	}
	return nums
}

java

public static int[] ShellSort(int[] arr){
    int gap=arr.length/2;
    while (gap>0){
        for (int i=gap;i<arr.length;i++){
            int cur=arr[i];
            int preIndex=i-gap;
            while (preIndex>=0&&arr[preIndex]>cur){
                arr[preIndex+gap]=arr[preIndex];
                preIndex-=gap;
            }
            arr[preIndex+gap]=cur;
        }
        gap/=2;
    }
    return arr;
}

分析

  • 稳定性:不稳定 ,分成几个序列单独插入排序了

  • 时间:O(nlogn)

    1. 增量序列的长度为 log⁡n。
    2. 每次插入排序的平均时间复杂度为 O(n)。
    3. 数组逐渐变得部分有序,减少了比较和交换的次数。
  • 空间:O(1)

5. 归并排序

将数组分成最小粒度的0/1个数的数组,进行merge排序

步骤

MergeSort(arr) []int{
    if length=1{
    	return []int{arr[0]}
    } 
	arr1=arr[0:length/2]
	arr2=arr[length/2:length]
	merge(MergeSort(arr1),MergeSort(arr2))
}

merge(arr1,arr2) []int{
	// 1. 创建arr1.length+arr2.length长度的数组
	// 2. 指针1(2)指向arr1(2)的首位
	// 3. 比较两指针数值大小,小的那个将数值移动到新建数组,指针后移;知道一个指针到达最后
	// 4. 另一数组剩余部分全部复制到新数组中
}

GO

func MergeSort(nums []int) []int{
    if len(nums)==1{
        return []int{nums[0]}
    }
    arr1:=nums[0:len(nums)/2]
    arr2:=nums[len(nums)/2:len(nums)]
    return merge(MergeSort(arr1),MergeSort(arr2))
} 

func merge(arr1,arr2 []int) []int{
    res:=make([]int,len(arr1)+len(arr2))
    left,right,index:=0,0,0
    for (left<len(arr1)&&right<len(arr2)){
        if arr1[left]<=arr2[right]{
            res[index]=arr1[left]
            left++
        }else{
            res[index]=arr2[right]
            right++
        }
        index++
    }
    if left<len(arr1){
        for left<len(arr1){
            res[index]=arr1[left]
            left++
            index++
        }
    }else{
        for right<len(arr2){
            res[index]=arr2[right]
            right++
            index++
        }
    }
    return res
}

JAVA

public static int[] MergeSort(int[] arr){
    if (arr.length==1){
        return arr;
    }
    int[] arr1=Arrays.copyOfRange(arr,0,arr.length/2);
    int[] arr2=Arrays.copyOfRange(arr,arr.length/2,arr.length);
    return merge(MergeSort(arr1),MergeSort(arr2));
}

public static int[] merge(int[] arr1,int[]arr2){
    int left=0,right=0,index=0;
    int[] res=new int[arr1.length+arr2.length];
    while (left<arr1.length && right<arr2.length){
        if (arr1[left]<=arr2[right]){
            res[index]=arr1[left];
            left++;
        }else{
            res[index]=arr2[right];
            right++;
        }
        index++;
    }
    if (left==arr1.length){
        while (right<arr2.length){
            res[index]=arr2[right];
            right++;
            index++;
        }
    }else{
        while (left<arr1.length){
            res[index]=arr1[left];
            index++;
            left++;
        }
    }
    return res;
}

分析

  • 稳定性:稳定。左右两个指针指向一样数值的情况下,会优先拍左边的。
  • 时间复杂度:O(nlogn)
  • 空间:O(n) 临时的数组和递归时压入栈的数据占用的空间:n + logn;所以空间复杂度为: O(n)

6. 快速排序

任意找一个基准,将比基准小的值排在左侧,比基准大的排在右侧。基准的位置就确定了。

递归对基准左边的数组和基准右边的数组进行快排。

步骤

func quicksort([]int arr,int left,int right){
    // 如果left=right就不需要再排了;上一轮的position就在right-1right一个值不需要排
    // 如果left=right+1:上一轮的position就是right
	if left<right{
		position=partition(arr,left,right)  //确定的基准的位置
		quicksort(left,position-1)
		quicksort(position+1,right)
	}
}
func partition(arr,left,right) int{
	pivot=arr[right]  //基准设为最右边
	idx=right  //idx指的是:idx左侧的值,都是比基准小的
	for i in left...right-1{
		// 遇到比pivot小的值,和idx交换,idx后移,以保证idx左侧都是比pivot小的
		if arr[i]<pivot{
			swap(arr[idx],arr[i])
			idx++
		}
		i++
	}
	swap[arr[idx],arr[right]]  // 基准的位置确定
	return idx
}

GO

func QuickSort(nums []int,left,right int){
	if left<right{
		position:=partition(nums,left,right)
		QuickSort(nums,left,position-1)
		QuickSort(nums,position+1,right)
	}
}
func partition(nums []int,left,right int) int{
	pivot:=nums[right]
	idx:=left
	for i:=left;i<right;i++{
		if nums[i]<pivot{
			nums[i],nums[idx]=nums[idx],nums[i]
			idx++
		}
	}
	nums[right],nums[idx]=nums[idx],nums[right]
	return idx
}

func main(){
	nums:=[]int{5, 3, 8, 4, 2}
	QuickSort(nums,0,4)
	fmt.Println(nums)
}

JAVA

public static void QuickSort(int[] arr,int left,int right){
    if (left<right){
        int position=partition(arr,left,right);
        QuickSort(arr,left,position-1);
        QuickSort(arr,position+1,right);
    }
}

public static int partition(int[] arr,int left,int right){
    int pivot=arr[right];
    int idx=left;
    for (int i=left;i<right;i++){
        if (arr[i]<pivot){
            swap(arr,i,idx);
            idx++;
        }
    }
    swap(arr,idx,right);
    return idx;
}

分析

  • 稳定性:不稳定。
  • 时间复杂度:O(nlogn)。最差O(n^2)
  • 空间:O(logn) 递归时压入栈的数据占用的空间

7. 堆排序

  1. 先从最后一个非叶子节点开始调整,建立大顶堆;一个heapLen作为全局变量,维护无序序列的长度
  2. 将堆顶移动和数组末尾交换,heapLen--
  3. 从堆顶重新调整堆。(由于此时是已经有序的情况,所以直接从堆顶向下一路调整即可)
  4. 继续2.3.步骤,直到全部调整完

步骤

int heapLen   //无序序列的长度

func buildHeap(arr){
	// 从第一个非叶子节点调整
	for i in arr.length/2-1;i>=0;i--{
		heapify(arr,i)
	}
}

func heapify(arr,i){
	largest=i
	left=2*1+1
	right=2*i+2
	// 这里要和heapLen进行比较
	if (left<heapLen && arr[left]>arr[largest]){
		largest=left
	}
	if (right<heapLen && arr[right]>arr[largest]){
		largest=right
	}
	if (i!=largest){
		swap(arr,i,largest)
		heapify(arr,largest)
	}
}

func heapSort(arr){
	heapLen=arr.length
	buildHeap(arr)
	for i:=arr.length-1;i>0;i--{
		swap(arr,0,i)
		heapLen--
		heapify(arr,0)
	}
}

GO

var heapLen int

func heapify(arr []int ,i int){
	largest:=i
	left:=2*i+1
	right:=2*i+2
	if left<heapLen && arr[left]>arr[largest]{
		largest=left
	}
	if right<heapLen && arr[right]>arr[largest]{
		largest=right
	}
	if i!=largest{
		arr[i],arr[largest]=arr[largest],arr[i]
		heapify(arr,largest)
	}

}

func buildHeap(arr []int){
	for i:=len(arr)/2-1;i>=0;i--{
		heapify(arr,i)
	}
}

func HeapSort(arr []int){
	heapLen=len(arr)
	buildHeap(arr)
	for i:=len(arr)-1;i>0;i--{
		arr[i],arr[0]=arr[0],arr[i]
		heapLen--
		heapify(arr,0)
	}
}

JAVA

static int heapLen;

    public static void heapify(int[] arr,int i){
        int largest=i;
        int left=i*2+1;
        int right=i*2+2;
        if (left<heapLen && arr[left]>arr[largest]){
            largest=left;
        }
        if (right<heapLen && arr[right]>arr[largest]){
            largest=right;
        }
        if (i!=largest){
            swap(arr,i,largest);
            heapify(arr,largest);
        }
    }

    public static void buildHeap(int[] arr){
        for (int i=arr.length/2-1;i>=0;i--){
            heapify(arr,i);
        }
    } 

    public static void HeapSort(int[] arr){
    	heapLen=arr.length;  //这一行一定要先执行
        buildHeap(arr);
        for (int i=arr.length-1;i>0;i--){
            swap(arr,i,0);
            heapLen--;
            heapify(arr,0);
        }
    }

8. 计数排序

步骤

  1. 得到数组最大值和最小值
  2. 将arr[i]出现的次数储存在新数组count[arr[i]-min]中。 count的长度是最大值-最小值+1
  3. count[i]+=count[i-1] 这里表示的是 arr[i+min]出现的最靠后的下标+1
  4. 从后向前遍历arr,利用arr[i]-min 找到这个大小数值在count[arr[i]-min]-1,即是它应该放置的位置,再将count[arr[i]-min]--

从后向前遍历是实现稳定的前提,一开始填入count是通过从左向右,再次遍历arr,从右向左赋值

GO

func CountingSort(arr []int) []int{
	minVal,maxVal:=arr[0],arr[0]
	for _,num:=range arr{
		if num>maxVal{
			maxVal=num
		}else if num<minVal{
			minVal=num
		}
	}
	count:=make([]int,maxVal-minVal+1)
	for _,num:=range arr{
		count[num-minVal]++
	}
	for i:=1;i<len(count);i++{
		count[i]+=count[i-1]
	}
	res:=make([]int,len(arr))
	for i:=len(arr)-1;i>=0;i--{
		idx:=count[arr[i]-minVal]-1
		res[idx]=arr[i]
		count[arr[i]-minVal]--
	}
	return res

}

JAVA

public static int[] CountingSort(int[] arr){
    int minVal=arr[0],maxVal=arr[0];
    for (int i=0;i<arr.length;i++){
        if (arr[i]>maxVal){
            maxVal=arr[i];
        }else if (arr[i]<minVal){
            minVal=arr[i];
        }
    }
    int[] count=new int[maxVal-minVal+1];
    for (int i=0;i<arr.length;i++){
        count[arr[i]-minVal]++;
    }
    for (int i=1;i<count.length;i++){
        count[i]+=count[i-1];
    }
    int[] res=new int[arr.length];
    for (int i=arr.length-1;i>=0;i--){
        int idx=count[arr[i]-minVal]-1;
        res[idx]=arr[i];
        count[arr[i]-minVal]--;
    }
    return res;
}

分析

由于需要新建最大值-最小值+1长度的count数组,所以使用计数排序,必须知道数值范围

  • 稳定
  • 时间:O(n+k) n是数组长度,k是最大值-最小值+1
  • 空间:O(n+k) count数组+res数组

9. 桶排序

步骤

桶排序和计数排序类似。只是计数排序直接按照maxVal-minVal作为count数组的长度,count数组记录该数值出现的次数。

桶排序:

  1. 指定好桶的大小,根据(max-min)/buket_size+1计算需要的桶的数量
  2. 遍历数组,idx=(arr[i]-min)/bucket_size放入对应的桶中
  3. 遍历桶,每个桶内的元素数量如果大于1,对桶内进行排序(选择其他的排序算法,最好是O(nlogn)的)
  4. 遍历桶,将桶内数据移到新的结果数组

GO

func BucketSort(arr []int,bucketSize int) []int{
	// 获得最大值和最小值
	minVal,maxVal:=arr[0],arr[0]
	for _,num:=range arr{
		if num>maxVal{
			maxVal=num
		}else if num<minVal{
			minVal=num
		}
	}
	// 创建桶
	buketNum:=(maxVal-minVal)/bucketSize+1
	bucket:=make([][]int,buketNum)
	// 将元素分配到桶内
	for _,num:=range arr{
		idx:=(num-minVal)/bucketSize
		bucket[idx]=append(bucket[idx],num)
	}
	// 桶内排序
	for _,b:=range bucket{
		if len(b)>1{
			QuickSort(b,0,len(b)-1)
		}
	}
	// 桶内元素移出
	res:=make([]int,0,len(arr))
	for _,b:=range bucket{
		res=append(res,b...)
	}

	return res
}

分析

  • 稳定性:取决于桶内排序方法的选择。

  • 时间复杂度:O(n+k)

    1. 分配元素到桶中:𝑂(𝑛)
    2. 对每个桶中的元素进行排序:𝑂(𝑛log⁡𝑛/𝑘)

    假设我们有𝑘个桶,且元素均匀分布在桶中,那么每个桶中的元素数量为𝑛/𝑘。如果使用O(mlogm) 的排序算法(例如快速排序或归并排序)对每个桶中的元素进行排序,那么每个桶的排序时间为:n/klogn/k,由于有k 个桶,总排序时间为:𝑂(nlogn/k)。当K接近n,这段时间复杂度就接近0

    1. 合并所有桶中的元素:O(k)
  • 如果选择其他排序方式,或者元素并没有平均分到桶内(例如都集中到一个桶),就达不到这个时间复杂度了。
  • 空间:O(n+k)

10. 基数排序

步骤

  1. 得到数组中的最大数,得到maxVal的位数,即是循环次数n
  2. 循环n次,遍历数组,得到余数,即是该位的数值(从后往前,因为高位的优先级大,需要最后排)。num=arr[i]/(10^n)%10,将数组按照该位的数值放入不同的桶。
  3. 遍历桶,将数值按顺序放回数组。

GO

func RadixSort(arr []int) []int{
	// 获得最大值
	maxVal:=arr[0]
	for _,num:=range arr{
		if num>maxVal{
			maxVal=num
		}
	}
	// 循环次数
	n:=0
	for maxVal!=0{
		maxVal/=10
		n++
	}
	// 基数桶
	radix:=make([][]int,10)
	divisor := 1
	for i:=0;i<n;i++{

		// 清空桶(复用内存)
		for j := 0; j < 10; j++ {
			radix[j] = radix[j][:0]
		}

		// 放入桶
		for _,num:=range arr{
			r := (num / divisor) % 10
			radix[r]=append(radix[r],num)
		}
		divisor*=10

		arr=make([]int,0,len(arr))
		//从桶取出
		for _,b:=range radix{	
			arr=append(arr,b...)
		}
	}
	return arr
}

分析

  • 稳定
  • 时间:O(n*k)
  • 空间:O(N+K)

关于数组传参

GO和JAVA传的都是值传递,结构体/引用的副本。

image-20250222163752236 image-20250222163808614