排序系列之——快速排序
快速排序是对冒泡排序的一种改进。
基本思想是基于分治法:
1、在待排序列表L[1···n]中任取一个元素pivot作为基准,通过一趟排序将待排序列表划分为独立的两部分L[1···k-1],L[k+1···n],使得L[1···k-1],中的所有元素都小于等于privot,L[k+1···n]中所有元素大于等于privot。则privot放在了其最终位置L(k)上。这个过程称之为一趟快速排序。
2、而后,分别递归对两个子表重复上述过程,直至每部分内只有一个元素或空为止,即所有元素都放在了其最终位置上。
快速排序里最主要的的就是其分治法,
以下程序采用两种分治方法。
方法一较为简单,代码如下:
1 //8 12 4 13 18 2 int Partion(int *arr, int len)//(基本分割) 3 { 4 int left = 0; 5 int right = len -1; 6 7 int key = arr[0];//以第一个元素为枢轴值,对表进行划分--->对于随机数这是可行的。 8 while( left < right) 9 { 10 while( left < right && arr[right] >= key) --right; //走到 4 11 arr[left] = arr[right]; //将4赋值给arr[left] 12 13 if( left >= right) 14 break; 15 16 while( left < right && arr[left] <= key) ++left; //走到 12 17 arr[right] = arr[left]; 18 } 19 arr[left] = key;//交换8 4之后8的位置为left 20 return left; 21 }
若有初始序列:3,、8、7、1、2、5、6、4,则排序过程大致如下:
1 初始:3 8 7 1 2 5 6 4 2 2_ 8 7 1 2_ 5 6 4 3 <- 4 2 8_ 7 1 8_ 5 6 4 5 -> 6 2 1_ 7 1_ 8 5 6 4 7 <- 8 2 1 7_ 7_ 8 5 6 4 9 -> 10 一趟排序之后: 11 2 1 3_ 7 8 5 6 4 //arr[high] == arr[left]
方法二采用两个指针索引一前一后逐步向后扫描的方法,代码如下:
1 /方法二: 2 //利用两个指针索引一前一后逐步向后扫描的方法。 3 //3 8 7 1 2 5 6 4 4 int Partion2(int *arr, int len) 5 { 6 int key = arr[0]; 7 int slow = 0, fast; 8 9 for(fast = slow; fast < len; ++fast) 10 { 11 //当快指针所指向的元素值小于枢轴值3 的时候,慢指针位置向前移,同时交换各自的元素值 12 if( arr[fast] < key) 13 { 14 ++slow; 15 swap( &arr[slow], &arr[fast]); 16 } 17 } 18 //此处不可以是swap(&key, arr[slow]) 因为key为局部变量。程序结束,key即失效。 19 swap(&arr[0], &arr[slow]); 20 return slow; 21 }
若有初始序列:3,、8、7、1、2、5、6、4,则排序过程大致如下:
1 slow 2 初始序列:3 8_ 7 1_ 2 5 6 4 3 high -> -> ->| 4 5 ->slow 6 for循环 :3 1_ 7 8_ 2 5 6 4 7 high->| 8 9 ->slow 10 :3 1 2_ 8 7_ 5 6 4 11 high-> -> -> 12 13 一次结束: 2_ 1 3_ 8 7 5 6 4 (swap(&arr[0], &arr[slow]))
快速排序代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <time.h> 5 #define N 41 6 7 void print_Arr(int *arr, int len) 8 { 9 int i; 10 for (i = 0; i < len; ++i) 11 { 12 printf("%4d", arr[i]); 13 } 14 printf(" "); 15 } 16 17 void init_Arr(int *arr, int len) 18 { 19 int i = 0; 20 while( i < len) 21 { 22 arr[i] = rand()%1000 ; 23 ++i; 24 25 } 26 } 27 28 void swap(int* left, int *right) 29 { 30 int tmp = *left; 31 *left = *right; 32 *right = tmp; 33 } 34 //方法一: 35 //8 12 4 13 18 36 int Partion1(int *arr, int len)//(基本分割) 37 { 38 int left = 0; 39 int right = len -1; 40 41 int key = arr[0];//以第一个元素为枢轴值,对表进行划分--->对于随机数这是可行的。 42 while( left < right) 43 { 44 while( left < right && arr[right] >= key) --right; //走到 4 45 arr[left] = arr[right]; //将4赋值给arr[left] 46 47 if( left >= right) 48 break; 49 50 while( left < right && arr[left] <= key) ++left; //走到 12 51 arr[right] = arr[left]; 52 } 53 arr[left] = key;//交换8 4之后8的位置为left 54 return left; 55 } 56 //方法二: 57 //利用两个指针索引一前一后逐步向后扫描的方法。 58 //3 8 7 1 2 5 6 4 59 int Partion2(int *arr, int len) 60 { 61 int key = arr[0]; 62 int slow = 0, fast; 63 64 for(fast = slow; fast < len; ++fast) 65 { 66 //当快指针所指向的元素值小于枢轴值3 的时候,慢指针位置向前移,同时交换各自的元素值 67 if( arr[fast] < key) 68 { 69 ++slow; 70 swap( &arr[slow], &arr[fast]); 71 } 72 } 73 //此处不可以是swap(&key, arr[slow]) 因为key为局部变量。程序结束,key即失效。 74 swap(&arr[0], &arr[slow]); 75 return slow; 76 } 77 78 void QuickSort(int *arr, int len) 79 { 80 if( len <= 1) 81 return; 82 if( len <= 10) //长度小于10的时候,插入排序效率较高 83 { 84 int pos, index; 85 int key; 86 for(pos = 1; pos < len; ++pos) 87 { 88 key = arr[pos]; 89 for(index = pos-1; index >= 0; --index) 90 { 91 if( arr[index] > key) 92 arr[index +1] = arr[index]; 93 else 94 break; 95 } 96 arr[index +1] = key; 97 } 98 } 99 else //len >=10 100 { 101 int k = Partion2(arr, len);//分离点 102 QuickSort(arr, k); //0~k-1 103 QuickSort(arr+k+1, len-k-1);//k+1 ~ len-1 104 } 105 } 106 107 int main(int argc, char const *argv[]) 108 { 109 srand(time(NULL)); 110 111 int arr[N]; 112 init_Arr(arr, N); 113 printf("Before: "); 114 print_Arr(arr, N); 115 116 QuickSort(arr, N); 117 printf("After "); 118 print_Arr(arr, N); 119 return 0; 120 }
性能分析:
空间效率:由于快排是递归的,需要借助一个递归工作栈来保存每一层递归调用的必要信息。 最好情况下为向上取整log(N+1),最坏情况下要进行n-1次递归调用,所以栈的深度为O(n)。因而,空间复杂度在最坏的情况下为O(n),最好情况下为O(logN)。
时间复杂度:快速排序的运行时间与划分是否对称有关。而后者又与具体使用的划分算法有关,一般情况下为O(N*logN).。
最坏情况下:初始待排序列基本有序或者基本逆序时,最坏的时间复杂度为O(N*N);
改进:当递归过程中划分得到的子序列的规模叫小时,不要再用快速排序,可以采用直接插入排序完成。
稳定性:不稳定。(相对次序有可能发生变化)
快速排序时所有内部排序算法中平均性能可能最优的排序算法。
快速排序一次排序的应用:快速排序——一次快排的应用(笔试&面试)
排序系列之——堆排序
堆排序是一种树形选择排序算法。它的特点是:在排序过程中,将L[1···n]视为一棵完全二叉树的顺序存储结构,利用完全二叉树中双亲结点和孩子结点之间的内在关系,在当前无需区中选择关键字最大的元素。
大顶堆:最大元素存放于根结点中,且 对其任一非根结点,它的值小于等于双亲结点值。 小顶堆的定义刚好与大顶堆相反。
1、堆排序的关键是构造初始堆。对于初始序列建堆,就是一个反复帅选的过程。n个结点的完全二叉树,最后一个结点是第[n/2]个结点的孩子。对第[n/2]个结点为根的子树筛选。使孩子树成为堆。
2、之后向前依次对各结点为根的子树进行筛选。即看该结点值是否大于等于其左右孩子的结点值。若不是,将左右孩子结点中较大值与值交换。但是交换之后可能会破坏下一级的堆,与实践继续采用上述方法构造下一级的堆,直到树根位置。
3、建立大顶堆之后,使堆顶元素与当前堆的最大一个元素值进行交换。
4、交换之后,当前堆结点数要减 1,并且需要调整以树根根结点的堆。
堆排代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <time.h> 5 #define N 23 6 7 //打印函数 8 void print_Arr(int *arr, int len) 9 { 10 int i; 11 for (i = 0; i < len; ++i) 12 { 13 printf("%4d", arr[i]); 14 } 15 printf(" "); 16 } 17 //初始化函数 18 void init_Arr(int *arr, int len) 19 { 20 int i = 0; 21 while( i < len) 22 { 23 arr[i] = rand()%1000 ; 24 ++i; 25 } 26 } 27 //交换数据 28 void swap(int* left, int *right) 29 { 30 int tmp = *left; 31 *left = *right; 32 *right = tmp; 33 } 34 35 //调整大顶堆 36 void AdjustDown(int *arr, int i, int len)//i代表结点 37 { 38 int left = 2*i+1;//左孩子 39 int flag = 0;//标记最大值结点的位置 40 41 while(left < len) 42 { 43 if(arr[left] > arr[i])//左孩子结点值较大 44 flag = left; 45 else//父结点值较大 46 flag = i; 47 48 if( left+1 < len && arr[left+1] > arr[flag]) //存在右孩子,并且右孩子值大于父亲结点与左孩子之间的较大值 49 flag = left+1; 50 51 if( flag != i) //若最大值结点不等于附近结点-->调整之 52 { 53 swap(&arr[flag], &arr[i]); 54 i = flag;//继续调整堆 55 left = 2*flag + 1; //继续调整堆 56 } 57 else 58 break; 59 } 60 } 61 62 //堆排序 63 void HeapSort(int *arr, int len) 64 { 65 //建立初始堆 66 int p = 0; 67 for(p = len/2-1; p >= 0; --p) //从第一个有孩子的结点建堆 68 { 69 AdjustDown(arr, p, len-1); 70 } 71 72 int end = len-1;//堆的最后一个元素的位置 73 while(end >= 0) 74 { 75 swap(&arr[0], &arr[end]); //将大顶堆堆顶元素与堆的最后一个元素交换 76 --end; //长度减1 77 AdjustDown(arr, 0, end);//调整为大顶堆 78 } 79 } 80 81 int main(int argc, char const *argv[]) 82 { 83 srand(time(NULL)); 84 85 int arr[N]; 86 init_Arr(arr, N); 87 printf("Before: "); 88 print_Arr(arr, N); 89 90 HeapSort(arr, N); 91 printf("After "); 92 print_Arr(arr, N); 93 return 0; 94 }
堆排序性能分析如下:
空间效率:仅使用于了常数个辅助单元,所以空间复杂度为O(1);
时间效率:在最好、最坏和平均情况下,算法的时间复杂度都为O(N*logN).
稳定性:不稳定的排序算法.
堆的应用【大顶堆的应用】:【剑指offer】面试题30:最小的 K 个数
排序系列之——归并排序
归并的含义是将两个或者两个以上的有序表组合成一个新的有序表。
步骤:
1、假定待排表含有n个记录,则可以视为 n 个有序的子表,每个表的长度为1,然后两两归并,得到n/2个长度为2或1的有序表;
2、再两两归并····,直到合并成一个长度为n的有序表为止,这种排序方法称为二路归并排序
示例如下:
1 初始关键字:(49) (38) (65) (97) (76) (13) (27) 2 3 一趟归并后: (38 49) (65 97) (13 76) (27) 4 5 二趟归并后: (38 49 65 97) (13 27 76) 6 7 三趟归并后: ( 13 27 38 49 65 76 97 )
归并程序代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <time.h> 5 const int N = 21; 6 7 //打印函数 8 void print_Arr(int *arr, int len) 9 { 10 int i; 11 for (i = 0; i < len; ++i) 12 { 13 printf("%4d", arr[i]); 14 } 15 printf(" "); 16 } 17 //初始化函数 18 void init_Arr(int *arr, int len) 19 { 20 int i = 0; 21 while( i < len) 22 { 23 arr[i] = rand()%1000 ; 24 ++i; 25 } 26 } 27 //交换数据 28 void swap(int* left, int *right) 29 { 30 int tmp = *left; 31 *left = *right; 32 *right = tmp; 33 } 34 35 //合并元素 36 void Merge(int *arr, int low, int mid, int high) 37 { 38 int *B = (int*)malloc( (N+1)*sizeof(int) ); //申请辅助数组B 39 //表arr的两段arr[low, ```, mid],arr[mid+1, ```, high]各自有序,将它们合并成一个有序表 40 int k =0; 41 for(k = low; k <= high; ++k) 42 B[k] = arr[k];//将arr中所有的元素复制到数组B中 43 44 int i=0, j=0; 45 for(i = low, j = mid+1, k=i; i<=mid && j<=high; k++ ) 46 { 47 if(B[i] < B[j]) //比较B的左右两段中的元素 48 arr[k] = B[i++]; //将较小者复制回arr中 49 else 50 arr[k] = B[j++]; 51 } 52 while( i <= mid)//若,第一个表未检测完,复制 53 arr[k++] = B[i++]; 54 while( j <= high) 55 arr[k++] = B[j++]; 56 } 57 58 //归并排序 59 void MergeSort(int *arr, int len)// 60 { 61 int low = 0; 62 int high = len-1; 63 if( low < high) 64 { 65 int mid =(low + high)/2; //从中间划分两个子序列 66 //注意这里的第二个参数为mid+1,第二个参数的意义在于归并排序的长度 67 //(由于中间位置的元素放在左侧归并数组中,因此长度+1) 68 MergeSort(arr, mid+1); //对左侧的mid个子序列进行递归排序 69 MergeSort(arr+mid+1, len-mid-1);//对右侧的len-mid-1个子序列元素进行递归排序 70 Merge(arr, low, mid, high);//合并元素 71 } 72 } 73 74 int main(int argc, char const *argv[]) 75 { 76 srand(time(NULL)); 77 78 int arr[N]; 79 init_Arr(arr, N); 80 printf("Before: "); 81 print_Arr(arr, N); 82 83 MergeSort(arr, N); 84 printf("After "); 85 print_Arr(arr, N); 86 return 0; 87 }
归并排序性能分析如下:
空间效率:Merge()操作中,由于辅助空间刚好要占用 n 个单元,凡是一趟归并后这些空间就被释放了,所以归并排序的空间复杂度为 O(N)。
时间效率:每一堂归并的时间复杂度为O(N),共需要进行 logN趟排序,所以算法的时间复杂度为O(N*logN).
稳定性:由于Merge()操作不会改变相同关键字记录的相对次序,所以二路归并排序算法是一个稳定的排序算法.
完毕。