github.com/qiuhoude/go-web@v0.0.0-20220223060959-ab545e78f20d/algorithm/datastructures/sort_/sort_test.go (about)

     1  package sort_
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  )
     7  
     8  /*
     9  冒泡排序
    10  只会操作相邻的两个数据。每次冒泡操作都会对相邻的两个元素进行比较,看是否满足
    11  大小关系要求。如果不满足就让它俩互换。一次冒泡会让至少一个元素移动到它应该在的位置,
    12  重复 n 次,就完成了 n 个数据的排序工作
    13  属于稳定性算法(相同值排序前后顺序不变)
    14  空间 O(1)
    15  最好 O(n)
    16  最坏 O(n^2)
    17  
    18  满有序度 = n*(n-1)/2
    19  逆序度 = 满有序度 - 初始有序度
    20  逆序度 = 就是需要要换的次数
    21  平均逆序度 = (满有序度 - 0)/ 2  = n*(n-1)/4
    22  平均复杂度 约等于 O(n*(n-1)/4) , n值很大时 O(n^2)
    23  
    24  */
    25  func bubbleSort(arr []int) {
    26  	n := len(arr)
    27  	if n <= 0 {
    28  		return
    29  	}
    30  	for i := 0; i < n; i++ {
    31  		flag := false // 提前退出标志
    32  		for j := 0; j < n-1; j++ {
    33  			if arr[j] > arr[j+1] {
    34  				arr[j], arr[j+1] = arr[j+1], arr[j]
    35  				flag = true // 有交换
    36  			}
    37  		}
    38  		if !flag {
    39  			break
    40  		}
    41  	}
    42  }
    43  
    44  func TestBubbleSort(t *testing.T) {
    45  	arr := []int{4, 5, 6, 3, 2, 1}
    46  	bubbleSort(arr)
    47  	fmt.Println(arr)
    48  }
    49  
    50  /*
    51  插入排序
    52  
    53  (后面排序算法思路可以借鉴)
    54  将数组中的数据分为两个区间,已排序区间和未排序区间,初始已排序区间只有一个
    55  元素,就是数组的第一个元素。插入算法的核心思想是取未排序区间中的元素,在已排序区间中
    56  找到合适的插入位置将其插入,并保证已排序区间数据一直有序。重复这个过程,直到未排序区
    57  间中元素为空,算法结束
    58  
    59  是稳定性排序
    60  空间 O(1)
    61  最好 O(n)
    62  最坏 O(n^2)
    63  平局 O(n^2)
    64  */
    65  func insertionSort(arr []int) {
    66  	n := len(arr)
    67  	if n < 1 {
    68  		return
    69  	}
    70  	for i := 1; i < n; i++ {
    71  		v := arr[i] // 记录要插入的值
    72  		j := i - 1  // 0~j 已排序区间
    73  		// 查找要插入的位置
    74  		for ; j >= 0; j-- {
    75  			if arr[j] > v {
    76  				arr[j+1] = arr[j] // 移动数据
    77  			} else {
    78  				break
    79  			}
    80  		}
    81  		arr[j+1] = v // 插入数据
    82  	}
    83  }
    84  
    85  func TestInsertionSort(t *testing.T) {
    86  	arr := []int{4, 5, 6, 3, 2, 1}
    87  	insertionSort(arr)
    88  	fmt.Println(arr)
    89  }
    90  
    91  /*
    92  选择排序
    93  
    94  选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次
    95  会从未排序区间中找到最小的元素,将其放到已排序区间的末尾
    96  
    97  非稳定性的排序 , 例如 [5,8,5,2,9] ,后面的的5就会和前面的5进行换位置
    98  空间 O(1)
    99  最好 O(n)
   100  最坏 O(n^2)
   101  平局 O(n^2)
   102  */
   103  
   104  func selectionSort(arr []int) {
   105  	n := len(arr)
   106  	if n < 1 {
   107  		return
   108  	}
   109  	for i := 0; i < n-1; i++ {
   110  		minIndex := i            // 最小值的位置
   111  		for j := i; j < n; j++ { // 寻找最小值位置
   112  			if arr[j] < arr[minIndex] {
   113  				minIndex = j
   114  			}
   115  		}
   116  		// 交换位置
   117  		arr[i], arr[minIndex] = arr[minIndex], arr[i]
   118  	}
   119  }
   120  
   121  func TestSelectionSort(t *testing.T) {
   122  	arr := []int{4, 5, 6, 3, 2, 1, 8, 7}
   123  	selectionSort(arr)
   124  	fmt.Println(arr)
   125  }
   126  
   127  /*
   128  归并排序
   129  
   130  把数组从中间分成前后两部分,然后对前后两部分分别排序,再将排好序的两部分合并在一起,这样整个数组就都有序了
   131  
   132  递推公式: mergeSort(arr[p:r]) = mergeSort(arr[p:mid]) + mergeSort(arr[mid+1:r])
   133  merge() 进行合并数组操作
   134  mid = (p + r) / 2
   135  终止条件 p>=r
   136  
   137  递归代码的时间复杂度也可以写成递推公式 T(a) = T(b) + T(c) + K
   138  
   139  T(1) = C; n=1 时,只需要常量级的执行时间,所以表示为 C。
   140  T(n) = 2*T(n/2) + n; n>1
   141  
   142  T(n) = 2*T(n/2) + n
   143  	= 2*(2*T(n/4) + n/2) + n = 4*T(n/4) + 2*n
   144  	= 4*(2*T(n/8) + n/4) + 2*n = 8*T(n/8) + 3*n
   145  	= 8*(2*T(n/16) + n/8) + 3*n = 16*T(n/16) + 4*n
   146  	......
   147  	= 2^k * T(n/2^k) + k * n
   148  	= 2^(log2(n)) * C+ log(n)*n
   149  	= 2*n * C 		 + log(n)*n
   150  	= nlog(n) + 2n*C
   151  
   152  当 T(n/2^k) = T(1) = C
   153  n/2^k = 1  => k = log2(n)
   154  
   155  所以任何情况下时间复杂度 O(nlog(n))
   156  
   157  空间复杂度
   158  2^x=n => x=log2(n)
   159  看上去要去要分配log2(n)次,每次需要分配n/2^k个空间, O(n * log2(n));
   160  但是同一时刻只最多只有n个空间被分配
   161  所以复杂度是 O(n)
   162  */
   163  func mergeSortEnter(arr []int) {
   164  	n := len(arr)
   165  	if n <= 1 {
   166  		return
   167  	}
   168  	mergeSort(arr, 0, n-1)
   169  }
   170  
   171  func mergeSort(arr []int, p, r int) {
   172  	if p >= r {
   173  		return
   174  	}
   175  	mid := p + ((r - p) >> 1)
   176  	// 分而治之
   177  	mergeSort(arr, p, mid)
   178  	mergeSort(arr, mid+1, r)
   179  	// 将 A[p...q] 和 A[q+1...r] 合并为 A[p...r]
   180  	merge(arr, p, mid, r) // 归并
   181  }
   182  
   183  func merge(arr []int, p, mid, r int) {
   184  	tmp := make([]int, r-p+1)
   185  	i := p       // 前数组的指针
   186  	j := mid + 1 // 后数组指针
   187  	k := 0
   188  	for ; i <= mid && j <= r; k++ {
   189  		if arr[i] <= arr[j] { // 前数组 等于 后数组, 取前数组的值,保证稳定性
   190  			tmp[k] = arr[i]
   191  			i++
   192  		} else {
   193  			tmp[k] = arr[j]
   194  			j++
   195  		}
   196  	}
   197  	// 剩余的数据,填充到数组中
   198  	for ; i <= mid; k++ {
   199  		tmp[k] = arr[i]
   200  		i++
   201  	}
   202  	for ; j <= r; k++ {
   203  		tmp[k] = arr[j]
   204  		j++
   205  	}
   206  	copy(arr[p:r+1], tmp)
   207  }
   208  
   209  func TestMergeSort(t *testing.T) {
   210  	arr := []int{4, 5, 6, 3, 2, 1, 8, 7}
   211  	mergeSortEnter(arr)
   212  	t.Log(arr)
   213  }
   214  
   215  /*
   216  快速排序
   217  
   218  如果要排序数组中下标从 p 到 r 之间的一组数据,我们选择 p 到 r 之间
   219  的任意一个数据作为 pivot(分区点)
   220  遍历 p 到 r 之间的数据,将小于 pivot 的放到左边,将大于 pivot 的放到右边,将 pivot 放
   221  到中间。经过这一步骤之后,数组 p 到 r 之间的数据就被分成了三个部分,前面 p 到 q-1 之间
   222  都是小于 pivot 的,中间是 pivot,后面的 q+1 到 r 之间是大于 pivot 的
   223  
   224  递推公式: quickSort(arr,p,r) = quickSort(arr,p,q-1) + quickSort(arr,q+1,r)
   225  递归出口 p>=r
   226  
   227  空间复杂度 O(1)
   228  最坏 O(n^2) 选取的 pivot的位置点是最大值或最小值
   229  平均 O(nlogn)
   230  
   231  利用的分治 和 分区的思想
   232  
   233  */
   234  
   235  func quickSortEnter(arr []int) {
   236  	n := len(arr)
   237  	if n <= 1 {
   238  		return
   239  	}
   240  	quickSort(arr, 0, n-1)
   241  }
   242  
   243  func quickSort(arr []int, p, r int) {
   244  	if p >= r {
   245  		return
   246  	}
   247  	q := partition(arr, p, r) // 获取区服点
   248  	quickSort(arr, p, q-1)
   249  	quickSort(arr, q+1, r)
   250  }
   251  
   252  /*
   253  进行分区
   254  思路: 也可以根据插入排序或选择排序的分区思路,把原区域分为
   255  [0:i] 比pivot小的值, [i+1:end] 比pivot大的值两个区域
   256  步骤: 1. 找arr[end] 为 pivot值
   257  	 2. 开始遍历每个元素,只要 <pivot 就放到左边区域的尾部进行替换,左边区域尾部的下标+1
   258  	 3. 最后 end 与 左边区域尾部下标值进行交换,
   259  	 4. 左边区域尾部下标值就是要找的位置
   260  
   261  
   262  选取pivot的优化思路:
   263  1. 三数取中法  从区间的首、尾、中间,分别取出一个数,然后对比大小,取这 3 个数的中间值作为分区点
   264  2. 随机法
   265  */
   266  func partition(arr []int, p, r int) int {
   267  	// 取 r位置为 pivot点
   268  	pivot := arr[r]
   269  	i := p // p ~ i 是<pivot
   270  	for j := p; j < r; j++ {
   271  		if arr[j] < pivot {
   272  			if j != i {
   273  				arr[j], arr[i] = arr[i], arr[j] //放到最后面
   274  			}
   275  			i++
   276  		}
   277  	}
   278  	arr[i], arr[r] = arr[r], arr[i]
   279  	return i
   280  }
   281  
   282  func TestQuickSort(t *testing.T) {
   283  	arr := []int{4, 5, 6, 3, 2, 1, 8, 7}
   284  	quickSortEnter(arr)
   285  	t.Log(arr)
   286  
   287  }
   288  
   289  // 如何在 O(n) 的时间复杂度内查找一个无序数组中的第 K 大元素
   290  func findMaxK(arr []int, k int) int {
   291  	n := len(arr)
   292  	if n < 1 || n < k {
   293  		return -1
   294  	}
   295  	k = n - k + 1 // 因为 partition 是左小右大的正序, 找第K大 等于 找第 n-k+1 小的元素
   296  	r := n - 1
   297  	p := partition(arr, 0, r)
   298  	for j := 0; j < n; j++ { // 最多查找 n 次,防止死循环
   299  		if p+1 == k {
   300  			return p
   301  		} else if k > p+1 { // 继续右边找
   302  			p = partition(arr, p+1, r)
   303  		} else { // 继续左边找
   304  			p = partition(arr, 0, p-1)
   305  		}
   306  	}
   307  	return -1
   308  }
   309  
   310  func TestFindMaxK(t *testing.T) {
   311  	arr := []int{4, 5, 6, 3, 2, 1, 8, 7, 1, 1}
   312  	k := 1
   313  	i := findMaxK(arr, k)
   314  	if i != -1 {
   315  		t.Log(arr[i])
   316  	}
   317  }
   318  
   319  /*
   320  分库分页
   321  
   322  业务折衷法-禁止跳页查询
   323  (1)用正常的方法取得第一页数据,并得到第一页记录的time_max
   324  (2)每次翻页,将order by time offset X limit Y,改写成order by time where time>$time_max limit Y
   325  以保证每次只返回一页数据,性能为常量。
   326  
   327  业务折衷法-允许模糊数据
   328  (1)将order by time offset X limit Y,改写成order by time offset X/N limit Y/N
   329  
   330  二次查询法
   331  (1)将order by time offset X limit Y,改写成order by time offset X/N limit Y (N是分库的数量)
   332  (2)找到最小值time_min
   333  (3)between二次查询,order by time between $time_min and $time_i_max
   334  (4)设置虚拟time_min,找到time_min在各个分库的offset,从而得到time_min在全局的offset
   335  (5)得到了time_min在全局的offset,自然得到了全局的offset X limit Y
   336  */