经典算法

 

算法简介

算法稳定性
稳定性是指在一组待排序记录中,如果存在任意两个相等的记录R和S,且在待排序记录中R在S前,如果在排序后R依然在S前,即它们的前后位置在排序前后不发生改变,则称为排序算法为稳定的

稳定的排序算法: 冒泡排序、插入排序、折半插入排序、归并排序、计数排序、桶排序、基数排序
不稳定的排序算法: 选择排序、希尔排序、快速排序、堆排序

 

时间复杂度

时间复杂度是指执行算法所需要的计算工作量,可以认为是对排序数据的总的操作次数

常见的时间复杂度有:常数阶O(1), 对数阶O(logn),  线性阶O(n),  线性对数阶O(nlogn),平方阶O(n^2)

  

空间复杂度

空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度, 即算法在执行时所需存储空间的度量

当一个算法的空间复杂度为一个常量, 即不随被处理数据量n的大小而改变时,可表示为O(1)

当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为O(log2n)

当一个算法的空间复杂度与n成线性比例关系时,可表示为0(n)

 

算法

 

时间复杂度
空间复杂度(需要额外的空间) 稳定性 说明
平均 最好 最坏
冒泡排序 O(n^2) O(n) O(n^2) T(1) 稳定 n: 待排序列长度
选择排序 O(n^2) O(n) O(n^2) T(1) 不稳定 n: 待排序列长度
插入排序 O(n^2) O(n) O(n^2) T(1) 稳定 n: 待排序列长度
希尔排序 O(n^1.3) O(n) O(n^2) T(1) 不稳定 n: 待排序列长度
快速排序 O(nlog2n) O(nlog2n) O(n^2) T(log2n)~T(n)  不稳定

n: 待排序列长度

1 每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1时,有最坏时间复杂度
2 每次划分所选择的中间数恰好将当前序乎等分时,有最好时间复杂度

归并排序 O(nlogn) O(nlogn) O(nlogn) T(n) 稳定 n: 待排序列长度n大时好,归并比较占用内存,内存随n的增大而增大,但却是效率高且稳定的排序算法
计数排序 O(n+k) O(n+k) O(n+k) T(k) 稳定

n: 待排序列长度

k: 待排序序列中的最大值k不大时好,快于任何比较排序算法,以空间换时间的算法,当O(k)>O(n*log(n)),其效率不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*log(n))

桶排序 O(n+n*logn-n*logm) O(n) O(n+n*logn-n*logm) T(n+m) 稳定

n: 待排序序列长度

m: 桶的个数

当n=m时,有最好的时间复杂度

基数排序 O(d(n+r)) O(d(n+r)) O(d(n+r)) T(d(n+r)) 稳定

n: 待排序列长度

d: 元素最长的位数

r(0~9): 每位取值范围

 

一、冒泡排序(Bubble Sort)

冒泡排序是基于交换的排序,它重复走过需要排序的元素,依次比较相邻的两个元素的大小,如果顺序错误则交换位置,直到没有相邻元素需要交换

 

1 原理

1) 每一趟(i)在未排序序列中[0:n-i-1]依次比较相邻的两个数,如果顺序错误则交换位置,每一趟结束时,确定一个本趟的最大值(最小值)
2) 重复步骤 1),直到i=n-1

n是未排序的序列中元素个数,每一趟只能确定一个数

 

2 时间复杂度
如果待排序序列的初始状态恰好是期望的排序结果, 需要进行n-1趟扫描,每趟所需的交换次数0, 最好的时间复杂度为O(n);

如果待排序序列初始状态和期望的排序反序,需要进行n-1趟排序,每趟所需的交换次数为n-1-i(i表示第几趟), 最坏时间复杂度为O(n^2)

冒泡排序的平均时间复杂度为O(n^2)

 

3 算法稳定性

比较是相邻的两个元素比较,交换也发生在这两个元素之间,如果两个元素相等,是不会交换位置

如果两个相等的元素没有相邻,那么即使通过交换把两个元素相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,冒泡排序是一种稳定排序算法 

 

4 golang代码实现

/*
BubbleSort ----冒泡排序,从小到大排序

1) 每一次(i)在未排序序列中[0:len(arr)-1-i]依次比较相邻的两个数,如果顺序错误则交换位置,每一趟结束时,确定一个本趟的最大值(最小值)
2) 重复步骤 1),直到i=len(arr)-1
 */
func BubbleSort(arr []int) {

    if len(arr) <= 1 {
        return
    }

    for i := 0; i < len(arr)- 1; i++ {
        for j := 0; j < len(arr)-1-i; j++ {
            if arr[j] > arr[j+1] {
                arr[j], arr[j+1] = arr[j+1], arr[j]
            }
        }
    }

    return
}

 

二、选择排序(Selection Sort)

选择排序是一种简单直观的排序算法,给每个位置选择当前未排序序列中的最大或最小值

 

1 原理
1) 每一次(i)从待排序序列[0:n]的未排序序列中[i:n-1]找到最小(大)元素,存放到已排序序列[0:i]的末尾
2) 重复步骤 1),直到所有元素均排序完毕

 

2 时间复杂度
n是待排序序列中元素个数

交换次数: 0~(n-1) = O(n)
比较次数: (n-1)+(n-2)+...+1 = n*(n-1)/2 = O(n^2)
赋值次数: 0~n-1= O(n)

最好情况,遍历n-1次, 交换0次;

最坏情况,遍历n-1次, 交换n-1次

交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快

选择排序的平均时间复杂度为O(n^2)

 

3 算法稳定性

在一趟选择中,如果一个元素比当前元素小,而该元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了,所以选择排序是一个不稳定的排序算法

如序列 [4, 5, 4, 2, 8, 9],第一趟选择中,第1个元素4和2交换,那么原序列中两个4的相对前后顺序就被破坏了

 

4 优化
在每一趟遍历中,同时找出最大值与最小值,放到数组两端,这样就能将遍历的趟数减少一半

 

5 golang代码实现

/*
SelectSort ----选择排序,从小到大排序

1) 每一次(i)从待排序序列[0:n]的未排序序列(右边区间)[i: len(arr)]中寻找最小元素,左边区间[0: i]已经排好序了,找到则交换位置
2) 重复步骤 1),直到i=n-1
*/
func SelectSort(arr []int) {

   if len(arr) <= 1 {
      return
   }

   for i := 0; i < len(arr)-1; i++ {
      //最小值元素的索引,每个位置(i),只需和它后面的元素[i+1, len(arr)]比较
      minIndex := i
      for j := i+1; j < len(arr); j++ {
         if arr[j] < arr[minIndex] {
            minIndex = j
         }
      }

      if i != minIndex {
         arr[i], arr[minIndex] = arr[minIndex], arr[i]
      }
   }

   return
}

/*
SimplifySelectSort ----优化选择排序,从小到大排序
同时找最大值和最小值,最小值放到前面,最大值放到后面

1) 每一次(i)从待排序序列的未排序序列[left, right+1]中同时找最大值和最小值,找到则把最小值放到left处,最大值放到right处
2) 重复步骤 1),直到left>=right
*/
func SimplifySelectSort(arr []int) {

   if len(arr) <= 1 {
      return
   }

   for left, right := 0, len(arr)-1; left < right; {
      minIndex := left
      maxIndex := right
      for i := left; i <= right; i++ {
         if arr[i] < arr[minIndex] {
            minIndex = i
         }
         if arr[i] > arr[maxIndex] {
            maxIndex = i
         }
      }

      arr[left], arr[right] = arr[minIndex], arr[maxIndex]
      left++
      right--
   }

   return
}

 

三、插入排序(Insertion Sort)

插入排序也称为直接插入排序。对于少量元素的排序,它是一个有效的算法

 

1 原理
1) 从无序部分取出一个元素(当前元素),与有序部分中的元素从后向前依次进行比较,并找到合适的位置(从后往前,第一个大于等于或小于等于当前元素的元素后面),将该元素插到有序组当中
2) 重复上面的步骤 1),直到所有元素都插入到正确的位置

 

2 时间复杂度
如果待排序序列的初始状态恰好是期望的排序结果,只需当前数跟前一个数比较,需要比较n-1次,时间复杂度为O(n)
如果待排序序列初始状态和期望的排序反序的,此时需要比较次数最多,总次数为: 1+2+3+…+n-1,时间复杂度为O(n^2)

插入排序的平均时间复杂度为O(n^2)

 

3 算法稳定性

插入排序是稳定的算法

每次把当前元素插入到有序序列中小于等于或大于等于当前元素的元素后面,所以相同的元素位置将保持原有位置不变

 

4 golang代码实现

/*
InsertionSort ----插入排序,从小到大排序

1) 从未排序序列中[i, len(arr)]取出第一个元素arr[i],与有序序列[0,i]的元素从后向前相邻的元素依次进行比较,顺序不对则交换位置,直到找到有序序列中小于或者等于arr[i]的元素
2) 重复步骤 1),直到i=len(arr)-1
 */
func InsertionSort(arr []int) {

    if len(arr) <= 1 {
        return
    }

    for i := 1; i < len(arr); i++ {
        for j := i-1; j >= 0 && arr[j] > arr[j+1]; j-- {
            arr[j], arr[j+1] = arr[j+1], arr[j]
        }
    }

    return
}

 

四、希尔排序(Shell's Sort)

希尔排序是插入排序的一种,又称"缩小增量排序"(Diminishing Increment Sort),是直接插入排序算法的一种更高效的改进版本,希尔排序是非稳定排序算法

 

1 原理
1) 取一个增量increment(一般是待排序序列长度的一半,向下取整)作为间隔,所有距离为increment的元素放在同一个逻辑数组中
2) 对每个逻辑数组进行插入排序,然后缩小间隔increment(每次缩小一半,向下取整)
3) 重复步骤 1), 2),直到最后increment=1,将所有元素放在同一个数组中排序为止

 

2 时间复杂度
最坏情况下的时间复杂度为O(n^2)
平均情况下的时间复杂度为O(n^1.3)

 

3 算法稳定性

一次插入排序是稳定的,不会改变相同元素的相对顺序

由于多次插入排序,在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的算法

 

4 golang代码实现

/*
ShellsSort ----希尔排序

1) 取一个增量increment(一般是待排序序列长度的一半,向下取整)作为间隔,所有距离为increment的元素放在同一个逻辑数组中
2) 对每个逻辑数组进行插入排序,然后缩小间隔increment(每次缩小一半,向下取整)
3) 重复步骤 1), 2),直到最后increment=1,将所有元素放在同一个数组中排序为止
 */
func ShellsSort(arr []int) {

    if len(arr) <= 1 {
        return
    }
  
    for increment := len(arr)/2; increment > 0; increment /= 2 {
        for i := increment; i < len(arr); i += increment {
            for j := i - increment; j >= 0 && arr[j] > arr[j+increment]; j -= increment {
                arr[j], arr[j+increment] = arr[j+increment], arr[j]
            }
        }
    }

    return
}

 

五、快速排序(Quick Sort)

快速排序通过多次比较和交换来实现排序

 

1 原理
1) 首先设定一个分界值,通过该分界值将数组分成左右两部分,小于分界值的数据集中到数组的左边,大于或等于分界值的数据集中到数组右边
2) 左边和右边的数据可以独立排序,对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值;右侧的数组数据也可以做类似处理
3) 重复步骤 1), 2), 通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左、右两个部分各数据排序完成后,整个数组的排序也就完成了

 

2 复杂度
理想的情况,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表,时间复杂度为O(nlog2n) 
最坏的情况,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。时间复杂度为O(n^2)

所以快速排序的平均时间复杂度是O(nlog2n),因此,快速排序被认为是目前最好的一种内部排序方法

 

3 算法稳定性

快速排序是不稳定的算法

如序列 [4, 3, 4, 2, 8, 9],以第一个元素4为分界值,第2个元素到第4个元素会在分界值的左边,原序列中两个4的相对前后顺序就被破坏了

 

4 golang代码实现

/*
QuickSort ----快速排序,从小到大排序
单路排序

1) 从未排序序列中取出一个元素作为基准值(一般情况下选择第一个元素)
2) 取未排序序列[0, len(arr)-1]的左边界索引head=0,右边界索引tail=len(arr)-1,从前往后比较,小于基准值的元素集中到左边,大于或等于基准值的元素集中到右边
3) 基准值的[0:head],右边[head+1:] 继续重复步骤1), 2),直到子数组长度小于等于1

arr[i] >= baseValue时不用i++,因为交换后的arr[i](此时arr[i]=arr[tail])的值不一定大于等于baseValue,需要再次进行比较
 */
func QuickSort(arr []int) {

    if len(arr) <= 1 {
        return
    }

    head := 0
    for i, baseValue, tail := 1, arr[0], len(arr)-1; head < tail; {
        if arr[i] < baseValue {
            arr[i], arr[head] = arr[head], arr[i]
            head++
            i++
        }   else {
            arr[i], arr[tail] = arr[tail], arr[i]
            tail--
        }
    }

    QuickSort(arr[:head])
    QuickSort(arr[head+1:])

    return
}

/*
SimplifyQuickSort ----双路快速排序,从小到大排序
双路排序,从右往左找小于基准值的元素,从左往右找大于基准值的元素

1) 从未排序序列中取出一个元素作为基准值(一般情况下选择第一个元素),
2) 取未排序序列[0, len(arr)-1]的左边界索引head=0,右边界索引tail=len(arr)-1,从后往前比较(tail--),找到第一个小于基准值的元素,交换位置(小于基准值的元素位于左边); 从前往后比较(head++),找到第一个大于基准值的元素,交换位置(大于基准值的元素位于右边)
3) 基准值的左边[0:head],右边[head+1:] 继续重复步骤1), 2),直到子数组长度小于等于1
 */
func SimplifyQuickSort(arr []int) {

    if len(arr) <= 1 {
        return
    }

    head := 0
    for baseValue, tail := arr[0], len(arr)-1; head < tail; {
        for arr[tail] > baseValue {
            tail--
        }
        arr[head], arr[tail] = arr[tail], arr[head]

        for arr[head] < baseValue {
            head++
        }
        arr[head], arr[tail] = arr[tail], arr[head]
    }

    SimplifyQuickSort(arr[:head])
    SimplifyQuickSort(arr[head+1:])

    return
}


六、归并排序(Merge Sort)

归并排序采用分治法(Divide and Conquer)的一个非常典型的应用,归并排序比较占用内存,但却是一种效率高且稳定的算法

 

1 原理
1) 将待排序序列分割成长度为 n/2 的若干个子数组
2) 子数组也根据其长度不断分割成为更小的子数组(n/2),直到不能分割
3) 最小子数组之间开始两两合并,合并之后的结果再合并
4) 重复步骤 3), 直到合并成为长度为 n 的已经排序的数组

 

2 时间复杂度
最好和最坏时间复杂度都为 O(nlogn)

 

3 算法稳定性

归并排序是稳定的算法

 

4 golang代码实现

/*
MergeSort ----归并排序

arr = []int{6, 202, 100, 301, 38, 8, 1}执行顺序:
程序执行顺序:
1) arr拆分,n=3: left_1=[]int{6, 202, 100}, right_1=[]int{301, 38, 8, 1}
2) left_1拆分,n=1: left_2=[]int{6}, right_2=[]int{202, 100}
3) right_2拆分,n=1: left_3=[]int{202}, right_3=[]int{100}
4) right_1拆分,n=2: left_4=[]int{301, 38}, right_4=[]int{8, 1}
5) left_4拆分,n=1: left_5=[]int{301}, right_5=[]int{38}
6) right_4拆分,n=1: left_6=[]int{8}, right_6=[]int{1}

调用merge()的顺序:
步骤3)调用merge()后right_2=[]int{100, 202}
步骤2)调用merge()后left_1=[]int{6, 100, 202}
步骤5)调用merge()后left_4=[]int{38, 301}
步骤6)调用merge()后right_4=[]int{1, 8}
步骤4)调用merge()后right_1=[]int{1, 8, 38, 301}
步骤1)调用merge()后arr=[]int{1, 6, 8, 38, 100, 202, 301}
 */
func MergeSort(arr []int) []int {

    if len(arr) <= 1 {
        return arr
    }
    n := len(arr)/2

    leftSubArr := MergeSort(arr[:n])
    rightSubArr := MergeSort(arr[n:])

    return merge(leftSubArr, rightSubArr)
}

func merge(leftSubArr, rightSubArr []int) (res []int) {

    l, r := 0, 0
    //循环比较,先添加较小的元素
    for l < len(leftSubArr) && r < len(rightSubArr) {
        if leftSubArr[l] < rightSubArr[r] {
            res = append(res, leftSubArr[l])
            l++
        }   else {
            res = append(res, rightSubArr[r])
            r++
        }
    }
    //添加大的元素
    res = append(res, append(leftSubArr[l:], rightSubArr[r:]...)...)

    return
}

  


七、计数排序(Counting Sort)

计数排序是一个非基于比较的排序算法,以空间换时间的算法

 

局限性:
1) 当待排序序列的最大值(k)过大时,并不适用于计数排序,因为计数排序需要创建长度为k的数组,k过大不但严重浪费了空间,而且时间复杂度也随之升高
2) 当待排序序列的元素不是整数时,并不适用于计数排序,因为计数排序是根据原数组的元素作为新数组的索引来存放,数组索引只能是整数

 

1 原理
1) 根据待排序序列中的最大元素,申请额外空间
2) 遍历待排序序列,将待排序序列中每一个元素的值作为索引,元素出现的次数作为值,记录到申请的额外空间中
3) 遍历额外空间中的数据,将每一个不为0的元素重新放到排序序列中

 

2 时间复杂度
时间复杂度是O(n+k),快于任何比较排序算法
当O(k)>O(n*logn)的时候其效率不如基于比较的排序(基于比较的排序的时间复杂度在理论上的下限是O(n*logn)

 

3 算法稳定性

计数排序算法是一个稳定的算法

 

4 golang代码实现

/*
CountingSort ----计数排序

1) 查找arr中的最大元素,根据最大元素+1作为中间数组(tmpArr)的长度
2) 遍历arr,将arr中每一个元素的值作为索引,元素出现的次数作为值,记录到tmpArr
3) 遍历tmpArr,将每一个不为0的元素重新放到arr
*/
func CountingSort(arr []int) {

    maxValue := 0
    length := len(arr)
    for i := 0; i < length; i++ {
        if arr[i] > maxValue {
            maxValue = arr[i]
        }
    }

    tmpArr := make([]int, maxValue+1)
    for i := 0; i < length; i++ {
        tmpArr[arr[i]] += 1
    }

    for i, j := 0, 0; i <= maxValue; i++ {
        for tmpArr[i] > 0 {
            arr[j] = i
            j++
            tmpArr[i]--
        }
    }

    return
}

  

八、桶排序(Bucket sort)

桶排序并不是比较排序,空间换时间的算法
适用于小范围整数数据,且独立均匀分布,可以计算的数据量很大,而且符合线性期望时间

 

1 原理
1) 将待排序序列划分为n个大小相等的子区间(桶),即划分为n个桶
2) 遍历待排序序列,将每个元素,按照桶的范围,分别放入不同的桶中
3) 使用插入排序或者其他排序方法对不同的桶中的元素进行排序,合并所有的桶,排序完成

 

2 时间复杂度
对于待排序序列长度为N,桶的个数为M,平均每个桶[N/M]个数据

桶排序平均时间复杂度为: O(N)+O(M*(N/M)*log(N/M))= O(N+N*(logN-logM))= O(N+N*logN-N*logM)
当N=M时,即每个桶只有一个数据时,桶排序的最好效率能够达到O(N)

桶排序的平均时间复杂度为线性的O(N+C),其中C=N*(logN-logM)。如果相对于同样的N,桶数量M越大,其效率越高,最好的时间复杂度达到O(N)
桶排序的空间复杂度为O(N+M),如果输入数据非常庞大,而桶的数量也非常多,则需要花费更大的空间代价

 

3 算法稳定性

桶排序是一种稳定的算法,不需要比较和交换

 

4 golang代码实现

/*
BucketSort ----桶排序

1) 将待排序序列划分为bucketCount个大小相等的子区间(桶),即划分为bucketCount个桶
2) 遍历待排序序列,将每个元素,按照桶的范围,分别放入不同的桶中
3) 使用插入排序方法对不同的桶中的元素进行排序,合并所有的桶,排序完成
*/
func BucketSort(arr []int, bucketCount int) {

    if len(arr) <= 1 {
        return
    }

    maxValue, minValue := arr[0], arr[0]
    for i := 1; i <len(arr); i++="" {="" if="" arr[i]=""> maxValue {
            maxValue = arr[i]
        }
        if arr[i] < minValue {
            minValue = arr[i]
        }
    }

    bucketRange := float64(maxValue-minValue+1)/float64(bucketCount)
    tmpArr := make([][]int, bucketCount)
    for i := 0; i < len(arr); i++ {
        val := arr[i]
        index := int(float64(val-minValue)/bucketRange)
        tmpArr[index] = append(tmpArr[index], val)
    }

    for i, j := 0, 0; i < len(tmpArr); i++ {
        length := len(tmpArr[i])
        if length > 0 {
            insertionSort(tmpArr[i])
            copy(arr[j:], tmpArr[i])
            j += length
        }
    }

    return
}

func insertionSort(arr []int) {

    if len(arr) <= 1 {
        return
    }

    for i := 1; i < len(arr); i++ {
        for j := i-1; j >= 0 && arr[j] > arr[j+1]; j-- {
            arr[j], arr[j+1] = arr[j+1], arr[j]
        }
    }

    return
}

 

九、基数排序(Radix Sort)

基数排序属于"分配式排序"(distribution sort), 又称"桶子法"(bucket sort), 空间换时间的算法

 

将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零,然后,从最低位开始,依次进行一次排序,从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列
空间换时间的算法

 

最高位优先(Most Significant Digit first, MSD)法: 由键值的最左边开始,即最高位开始

最低位优先(Least Significant Digit first, LSD)法: 由键值的最右边开始,即最低位开始

 

1 原理
1) 获取待排序列中所有元素的最高位数
2) 从最低位开始,依次进行排序
3) 重复步骤2), 直到最高位排序完成

 

2 时间复杂度
待排序列长度为n,元素最长的位数为d,每位取值范围是r(0~9)
时间复杂度为O(d(n+r))
其中,一趟分配时间复杂度为O(n),一趟收集时间复杂度为O(r),共进行d趟分配和收集

 

3 算法稳定性

基数排序法是一种稳定性的算法

 

4 golang代码实现

/*
RadixSort ----基数排序

1) 获arr中所有元素的最高位数
2) 从最低位(个位)开始,依次进行一次排序
3) 重复步骤2), 直到最高位排序完成
 */
func RadixSort(arr []int) {

    if len(arr) <= 1 {
        return
    }

    maxValue := arr[0]
    for i := 1; i <len(arr); i++="" {="" if="" arr[i]=""> maxValue {
            maxValue = arr[i]
        }
    }

    maxLength := len(strconv.Itoa(maxValue))
    tmpArr := make([][]int, 10)
    for i, n := 0, 1; i < maxLength; i++ {
        for j := 0; j < len(arr); j++ {
            digit := arr[j] / n % 10
            tmpArr[digit]  = append(tmpArr[digit] , arr[j])
        }

        for i, j := 0, 0; i < len(tmpArr); i++ {
            length := len(tmpArr[i])
            if length > 0 {
                copy(arr[j:], tmpArr[i])
                j += length
                tmpArr[i] = []int{}
            }
        }

        n *= 10
    }

    return
}

  

posted @ 2022-10-04 15:58  junffzhou  阅读(51)  评论(0编辑  收藏  举报