排序算法学习总结
本文中的思路代码皆借鉴于《大话数据结构》
冒泡排序法
算法描述:
给一个待排序的数组,固定其中一端,从另一端开始两两比较,如果反序则交换,直到没有反序的记录为止。在此过程中,数字较小(或较大)的慢慢从数组的一端向另一端移动,如同气泡慢慢浮上水面。
代码实现:
boolean flag = true;
public void sort() {
for (int i = 0; i < array.length - 1 && flag; i++) {
//每次循环初始为false,若在一个循环里没有发生数据交换,则排序结束。
flag = false;
for (int j = array.length - 1; j > i; j--) {
if (array[j - 1] > array[j]) {
Swap.swap(array, j - 1, j);
flag = true;
}
}
}
}
复杂度分析:
- 对于长度为N的数组,在初始有序的情况下,循环次数为N-1,交换次数为0;
- 初始反序的情况下,循环次数为(N-1)N/2次,交换次数为(N-1)N/2;
简单选择排序
算法描述:
每一次循环在n-i+1个记录里选取最小的记录,并和第i个记录交换,作为有序序列的第i个记录。
代码实现:
public void sort( ){
int min = 0;
for(int i = 0;i<length();i++){
min = i;
for(int j = i+1;j<length();j++){
if(array[min]>array[j]){
min = j;
}
}
if(min!=i){
Swap.swap(array, min, i);
}
}
}
复杂度分析:
- 给定一个长为N的数组,若初始为有序,则循环次数为(N-1)N/2,交换次数为0;
- 若初始反序,则循环次数为(N-1)N/2,交换次数为N/2;
直接插入排序
算法描述:
将一个数据插入到已经排序好的有序数组中。
代码实现:
public void sort(){
int temp = 0;
int i,j=0;
for(i = 1;i<length();i++){
if(array[i]<array[i-1]){
temp = array[i];//记录待插入的数
for(j = i-1;j>=0&&array[j]>temp;j--){
array[j+1] = array[j];//比待插入的数大的往后移
}
array[j+1] = temp;//将待插入的数插入正确的位置
}
}
}
复杂度分析:
- 给定一个长为N的数组,若初始为有序,则比较次数为N-1,移动次数为0;
- 若初始反序,则比较次数为(N-1)+(N-1)N/2=(N+2)(N-1)/2次,移动次数为(N+2)(N-1)/2次。
希尔排序
算法描述:
将待排序数组分成几块,对每一块进行直接插入排序,当整个序列基本有序后,再进行一次总的直接插入排序。
代码实现:
public void sort() {
int i, j = 0;
int temp = 0;
//increment为增量,初始为数组长度,代表将每个数据看做一个子块,再在每次循环中减小increment的大小,即将数组看成若干块,直至为1循环结束,排序结束
int increment = length();
do {
increment = increment / 3 + 1;//经验公式
for (i = increment ; i < length(); i++) {
if (array[i - increment] > array[i]) {
temp = array[i];
for (j = i - increment; j >= 0 && array[j] > temp; j -= increment) {
array[j + increment] = array[j];
}
array[j + increment] = temp;
}
}
} while (increment > 1);
}
复杂度分析:
时间复杂度为O(n^(3/2))
堆排序
算法分析:
堆排序就是利用堆(如大顶堆)进行排序的方法。它的基本思想是将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根节点。将其与序列最后一个数交换,此时最后一个数就是最大值。然后将剩余n-1个数构造成一个大顶堆,重复上述过程,完成排序。
代码实现:
/***
* 堆调整
* @param node 待调整的节点
* @param end 堆的最后一个节点
*/
private void heapAdjust(int node , int end){
int temp = array[node];
//对于节点i,若有子节点,则第2i+1个节点为其左节点,2(i+1)个节点为其右节点
for(int i = node*2+1;i<end;i=2*i+1){
if(i<end-1&&array[i]<array[i+1]) i++;//若右节点较大,则与右节点互换
if(temp>array[i]) break;//若根节点更大,则不调整,结束循环
array[node] = array[i];
node = i;
}
array[node] = temp;
}
public void sort(){
//构建大顶堆
for(int i = length()/2-1;i>=0;i--){
heapAdjust(i,length());
}
//排序
for(int j = length()-1;j>0;j--){
Swap.swap(array, j, 0);
heapAdjust(0,j);
}
}
复杂度分析:
- 构建堆时,每个非终端节点最多进行两次比较和互换,因此构建堆的时间复杂度为O(n);
- 排序时,第i次取堆顶记录重建堆要用O(logi)的时间(完全二叉树的某节点i到根节点的距离为logi+1),并且需要取n-1次堆顶数据,因此时间复杂度为O(nlogn);
- 故总体来说,堆排序的时间复杂度为O(nlogn)。
归并排序
算法分析:
假设初始序列有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到n/2个长度为2或1的有序子序列,再两两归并……直至得到一个长度为n的有序序列为止,此为2路归并排序。
代码实现:
public void sort() {
mergeSort(array, array, 0, length() - 1);
}
//将sr[s..t]归并排序为tr1[s..t]
public void mergeSort(int[] sr, int[] tr1, int s, int t) {
int m = (s + t) / 2;//将sr[s..t]平分为sr[s..m]和sr[m+1..t]
int[] tr2 = new int[length()];
if (s == t) {
tr1[s] = sr[s];
} else {
mergeSort(sr, tr2, s, m);
mergeSort(sr, tr2, m + 1, t);
merge(tr2, tr1, s, m, t);//将tr[s..m]和sr[m+1..t]归并待tr1[s..t]
}
}
public void merge(int[] sr, int[] tr, int s, int m, int t) {
int j, k, l;
//将sr中记录由小到大归并到tr
for (j = m + 1, k = s; s <= m && j <= t; k++) {
if (sr[s] < sr[j]) {
tr[k] = sr[s++];
} else {
tr[k] = sr[j++];
}
}
//将剩余的sr[s..m]复制到tr
if (s <= m) {
for (l = k ; l <= t; l++) {
tr[l] = sr[s++];
}
}
//将剩余的sr[j..n]复制到tr
if (j <= t) {
for (l = k ; l <= t; l++) {
tr[l] = sr[j++];
}
}
}
复杂度分析:
- 一趟归并需要将sr[0]~sr[n-1]中相邻的长度为h的有序序列进行两两归并。并将结果放到tr[0]~tr[n-1]中,这需要将待排序列中的所有记录扫描一遍,因此耗费O(n)时间;
- 由完全二叉树的深度可知,整个归并排序需要进行logn次;
- 故总的时间复杂度为O(nlogn)。
- 归并过程中需要与待排序序列同样大小的存储空间以及递归深度为logn的栈空间,因此空间复杂度为O(n+logn)。
- 此外,因为需要两两比较,不存在跳跃,因此是一种稳定的排序算法。
归并排序的非递归实现:
public void sort(int[] tr) {
int k = 1;
while (k < length()) {
mergePass(array, tr, k, length() - 1);
k = k << 1;
mergePass(tr, array, k, length() - 1);
k = k << 1;
}
}
public void mergePass(int[] sr, int[] tr, int s, int n) {
int i = 0;
while (i < n - 2 * s + 1) {
merge(sr, tr, i, i + s - 1, i + 2 * s - 1);//两两归并
i = i + 2 * s;
}
if (i <= n - s + 1) {//归并最后两个序列
merge(sr, tr, i, i + s - 1, n);
} else {//最后只剩一个序列
for (int j = i; j < n; j++) {
tr[j] = sr[j];
}
}
}
复杂度分析:
- 非递归的迭代方法,避免了递归时深度为logn的栈空间,因此空间复杂度为O(n),避免地柜时间性能上也有一定的提升(递归时函数进出栈需要消耗时间)。
快速排序
算法分析:
通过一趟排序将待排序记录分割成独立的两部分,其中一部分均比另一部分小,则可分别对这两部分继续进行排序,直至整个序列有序。
代码实现:
public void sort(){
qSort(0, length()-1);
}
protected void qSort(int low , int high){
int pivot;
if(low<high){
pivot = partition(low,high);//将array[low..high]分为两部分,在枢纽值pivot之前的都比其小,在其后的都比其大
qSort(low, pivot-1);
qSort(pivot+1, high);
}
}
protected int partition(int low , int high){
int pivotKey = array[low];//将待排序部分的第一个值作为枢纽值
while(low<high){//从两端交替向中间扫描
while(low<high&&array[high]>pivotKey)
high--;
Swap.swap(array, low, high);//将比枢纽值小的交换到前端
while(low<high&&array[low]<=pivotKey)
low++;
Swap.swap(array, low, high);//将比枢纽值大的交换到后端
}
//最终low=high,返回枢纽值
return low;
}
复杂度分析:
- 在最优情况下,即每次枢纽值都能将待排序序列平分两半,时间复杂度为O(nlogn),递归造成的栈空间为递归树的深度logn,即空间复杂度为O(logn);
- 在最坏情况下,即待排序序列为正序或倒序,时间复杂度为O(n^2),需要进行n-1次调用,空间复杂度为O(n);
- 平均情况下,时间复杂度为O(nlogn),空间复杂度为O(logn);
- 由于关键字的比较和交换是跳跃进行的,所以快速排序是一种不稳定的排序方法。
优化:
- 枢纽选取优化:取头、尾和中间值中的中间值
int mid = low+(high-low)/2;
if(array[low]>array[high])//左值较小
Swap.swap(array, low, high);
if(array[mid]>array[high])//右值最大
Swap.swap(array, mid, high);
if(array[mid]>array[low])//中值最小
Swap.swap(array, mid, high);
int pivotKey = array[low];
- 不必要的交换优化:替换操作取代交换操作
int pivotKey = array[low];
while(low<high){
while(low<high&&array[high]>pivotKey)
high--;
array[low] = array[high];
while(low<high&&array[low]<=pivotKey)
low++;
array[high] = array[low];
}
array[low] = pivotKey;
- 优化递归操作:尾递归
protected void qSort(int low , int high){
int pivot;
while(low<high){//if改成了while
pivot = partition(low,high);
qSort(low, pivot-1);
low = pivot+1;//尾递归
//qSort(pivot+1, high);
}
}
7种算法的指标:
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | o(n^2) | O(1) | 稳定 |
简单选择排序 | O(n^2) | O(n^2) | o(n^2) | O(1) | 稳定 |
直接插入排序 | O(n^2) | O(n) | o(n^2) | O(1) | 稳定 |
希尔排序 | O(nlogn)~O(n^2) | O(n^1/3) | o(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | o(n^2) | O(logn)~O(n) | 不稳定 |