十大排序
概念
本文说的排序方法都属于内部排序,即数据量小,足以在内存空间完成排序。
分类:
- 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破
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)
- 增量序列的长度为 logn。
- 每次插入排序的平均时间复杂度为 O(n)。
- 数组逐渐变得部分有序,减少了比较和交换的次数。
-
空间: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-1,right一个值不需要排
// 如果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. 堆排序
- 先从最后一个非叶子节点开始调整,建立大顶堆;一个heapLen作为全局变量,维护无序序列的长度
- 将堆顶移动和数组末尾交换,heapLen--
- 从堆顶重新调整堆。(由于此时是已经有序的情况,所以直接从堆顶向下一路调整即可)
- 继续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. 计数排序
步骤
- 得到数组最大值和最小值
- 将arr[i]出现的次数储存在新数组count[arr[i]-min]中。 count的长度是最大值-最小值+1
- count[i]+=count[i-1] 这里表示的是 arr[i+min]出现的最靠后的下标+1
- 从后向前遍历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数组记录该数值出现的次数。
桶排序:
- 指定好桶的大小,根据
(max-min)/buket_size+1计算需要的桶的数量 - 遍历数组,
idx=(arr[i]-min)/bucket_size放入对应的桶中 - 遍历桶,每个桶内的元素数量如果大于1,对桶内进行排序(选择其他的排序算法,最好是O(nlogn)的)
- 遍历桶,将桶内数据移到新的结果数组
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)
- 分配元素到桶中:𝑂(𝑛)
- 对每个桶中的元素进行排序:𝑂(𝑛log𝑛/𝑘)
假设我们有𝑘个桶,且元素均匀分布在桶中,那么每个桶中的元素数量为𝑛/𝑘。如果使用O(mlogm) 的排序算法(例如快速排序或归并排序)对每个桶中的元素进行排序,那么每个桶的排序时间为:n/klogn/k,由于有k 个桶,总排序时间为:𝑂(nlogn/k)。当K接近n,这段时间复杂度就接近0
- 合并所有桶中的元素:O(k)
- 如果选择其他排序方式,或者元素并没有平均分到桶内(例如都集中到一个桶),就达不到这个时间复杂度了。
- 空间:O(n+k)
10. 基数排序
步骤
- 得到数组中的最大数,得到maxVal的位数,即是循环次数n
- 循环n次,遍历数组,得到余数,即是该位的数值(从后往前,因为高位的优先级大,需要最后排)。
num=arr[i]/(10^n)%10,将数组按照该位的数值放入不同的桶。 - 遍历桶,将数值按顺序放回数组。
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传的都是值传递,结构体/引用的副本。
