插入排序 插入排序是指每次排序的元素插入到已排序的列表中,和洗扑克牌一样。对于顺序表,插入所花时间很多。
直接插入排序 从左到右选择元素插入到已排序列表,插入时保证使已排序列表依然有序。初始已排序列表为空
基本算法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 bool insertionSort (int arr[], const int len) { for (int i = 1 ; i < len; i++) { auto j = i; auto tmp = arr[i]; while (j != 0 && arr[j - 1 ] > tmp) { arr[j] = arr[j - 1 ]; j--; } arr[j] = tmp; pArr (arr, len); } return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 template <typename Container>int insertionSort (Container& container) { auto first = std::begin (container); auto end = std::end (container); if (first == end) return 0 ; for (auto i = std::next (first); i != end; i++) { auto key = *i; auto j = i; while (j != first && (*std::prev (j) > key)) { *j = *std::prev (j); j--; } *j = key; } return 0 ; }
1 2 Average time for 10000 runs and 1000 elements: 0 .00060332 secondsTotal time:6 .03324770
直接插入排序有两个循环,所以一般情况下时间复杂度是$O(n)*O(n)=O(n^2)$,最坏的情况是完全逆序,与平均相同,最好情况为顺序,内循环不需要比较,所以时间复杂度为$O(n)$ 对于空间复杂度,需要额外空间存放一个元素,所以是$O(1)$
二分插入排序 也叫折半插入排序,直接插入排序使用了顺序查找,效率较低,所以把顺序查找换成二分查找能提高内循环效率,然而还是插入,感觉没有提高多少效率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 bool binaryInsertionSort (int arr[], const int len) { for (int i = 1 ; i < len; i++) { auto high = i; auto low = 0 ; auto mid = 0 ; auto tmp = arr[i]; while (low < high) { mid = (low + high) / 2 ; if (arr[mid] < tmp) { low = mid + 1 ; } if (arr[mid] > tmp) { high = mid; } if (arr[mid] == tmp) { high = low = mid; break ; } } for (int j = i; j > low; --j) { arr[j] = arr[j - 1 ]; } arr[low] = tmp; pArr (arr, len); } return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 template <typename Container>bool binaryInsertionSort (Container& container) { auto first = std::begin (container); auto end = std::end (container); if (first == end) return false ; for (auto i = std::next (first); i != end; ++i) { auto high = i; auto low = first; auto mid = first; auto tmp = *i; while (low < high) { mid = low; std::advance (mid, std::distance (low, high) / 2 ); if (*mid < tmp) { low = std::next (mid); } else { high = mid; } } for (auto j = i; j > low; --j) { *j = *std::prev (j); } *low = tmp; } return true ; }
1 2 Average time for 10000 runs and 1000 elements: 0 .00054250 secondsTotal time:5 .42496460
我多试了几次(20长度),发现比直接插入的速度慢,难道是我写错了?QAQ,可能是算法太菜了。
外循环$O(n)$,内循环查找+插入$O(log_2n)+O(n)$,所以总体时间复杂度:$O(nlog_2n)+O(n^2)=O(n^2)$,对于最好时间,已排序完成,$O(n)$,最差时间,倒序$O(n^2)$
这么一看在n小时用直接插入,n大时用二分插入更好。 对于空间复杂度,由于有4个额外空间,所以是$O(4)$
希尔排序 又叫缩小增量排序。直接插入排序有两个性质,在快排序好时,效率高,大概能到O(n),但是插入操作十分低效因为每次都只能移动一位。所以希尔排序先划分小块分别插入排序,然后再总体插入排序。
那么选取H,即间隔就很重要了,这里有两篇文章
希尔排序 - OI Wiki 希尔排序 - 维基百科,自由的百科全书
都提到了比较好的H选择,因为Sedgewick步长有点不好写,这里直接就用原作者的排序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 bool shellSort (int arr[], const int len) { std::deque<int > gap = {}; auto gapinit = [&gap](int len, auto function) { for (int i = 0 ;; i++) { int x = function (i); if (x > len) break ; gap.insert (gap.begin (), x); } }; auto sort = [](int arr[], int gap, const int len) { for (int i = gap; i < len; i++) { int j = i; auto tmp = arr[i]; while (j - gap > 0 && arr[j - gap] > tmp) { arr[j] = arr[j - gap]; j -= gap; } arr[j] = tmp; } return 0 ; }; gapinit (len, [](int i) { return (int )pow (2 , i); }); for (auto g : gap) { sort (arr, g, len); pArr (arr, 20 ); } return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 template <typename Container>bool shellSort (Container& arr) { std::deque<int >gap = {}; auto first = std::begin (arr); auto end = std::end (arr); auto len = std::distance (first, end); auto gapinit = [&gap](int len, std::function<int (int )> func) { for (int i = 0 ;; i++) { int x = func (i); if (x > len) break ; gap.insert (gap.begin (), x); } }; auto sort = [first, end](Container& container, int gap) { auto len = std::distance (first, end); for (int i = gap; i < len; ++i) { auto tmp = (*(first + i)); int j; for (j = i; j >= gap && *(first + j - gap) > tmp; j -= gap) { *(first + j) = *(first + j - gap); } *(first + j) = (tmp); } }; gapinit (len, [](int i) { return std::pow (2 , i); }); for (auto g : gap) { sort (arr, g); pArr (arr); } return true ; }
时间测试
1 2 Average time for 10000 runs and 1000 elements: 0 .00008865 secondsTotal time:0 .88646020
可以看出,速度相较之前的两个提升了5倍不止。
对于希尔算法的时间复杂度,需要额外考虑gap的值,不同的值对应不同的复杂度,比如说增量为2的倍数,那么同一个组中2的倍数会多次比较,改成pow(3,i)
后
1 2 Average time for 10000 runs and 1000 elements: 0 .00007897 secondsTotal time:0 .78967320
很稳定地提高了大约0.1s。所以只能说大致来看时间复杂度在$O(nlog(n))$,对于以2的平方为间隔的,在完全倒序的情况下最差会退化到$O(n^2)$。查资料的时候发现,这是世界上第一个突破$O(n^2)$的排序算法,真强。 对于空间复杂度,由于额外占用一个tmp,所以为$O(1)$
交换排序 交换排序是指这一次排序的元素与其它元素交换最后实现的排序
冒泡排序 稳定的排序
经典老熟人,就不多说了,直接上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bool bubbleSort (int arr[], const int len) { for (int j = len; j > 0 ; j--) { bool sorted = true ; for (int i = 0 ; i < j - 1 ; i++) { if (arr[i] > arr[i + 1 ]) { sorted = false ; std::swap (arr[i], arr[i + 1 ]); } } if (sorted) break ; } return true ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 template <typename Container>bool bubbleSort (Container& container) { bool sorted = false ; for (auto end = std::end (container); !sorted && end != std::begin (container); --end) { sorted = true ; for (auto it = std::begin (container); std::next (it) != end; ++it) { if (*it > *std::next (it)) { std::swap (*it, *std::next (it)); sorted = false ; } } pArr (container); } return true ; }
测试:
1 2 Average time for 10000 runs and 1000 elements: 0 .00132296 secondsTotal time:13 .22964510
很慢,可能只适合小数组,定死的两重循环让它只能在$O(n^2)$上,除了运气很好刚好有序,则只需$O(n)$
空间复杂度$O(1)$
快速排序 顾名思义,很快的排序。每次排序时,定义一个基准值(一般是开头元素或结尾元素)然后从左和右分别引出指针,基准远离的那个指针开始向中心移动,遇到小于基准值的就复制到离基准近的位置,然后近的指针移动找大于基准值的,复制到远离基准的指针位置,两个指针相遇后,将基准赋值到这里。然后分别对左边和右边执行这个操作。因此要使用递归。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 bool quickSort (int arr[], const int len) { enum di { left, right }; std::function<void (int , int )> sort; sort = [&arr, &sort](int is, int ie) { pArr (arr, 20 ); if (is == ie) return ; if (is + 1 == ie) { if (arr[is] > arr[ie]) std::swap (arr[is], arr[ie]); return ; } di diriction = right; int ieb = ie; int isb = is; auto base = arr[is]; while (is < ie) { if (diriction == right) { while (base <= arr[ie] && is < ie) ie--; arr[is] = arr[ie]; diriction = left; } else { while (base > arr[is] && is < ie) { is++; } arr[ie] = arr[is]; diriction = right; } } arr[is] = base; if (is > isb) sort (isb, is - 1 ); if (is < ieb) sort (is + 1 , ieb); }; sort (0 , len - 1 ); return true ; }
后面就不写模板了QAQ
1 2 Average time for 10000 runs and 1000 elements: 0 .00006714 secondsTotal time:0 .67141270
可以看出确实很快,比希尔又少了0.1s左右。由于每次是折半,所以速度是$O(nlog(n))$,对应的,由于有折半次的递归,所以空间复杂度是$O(logn)$
选择排序 简单选择排序 树形选择排序 堆排序 归并排序 基数排序 外部排序