1、排序算法概要
注:基数排序的复杂度中,r代表关键字的基数,d代表长度,n代表关键字的个数。
2、 排序算法——冒泡排序冒泡排序是非常容易理解和实现,,以从小到大排序举例:
设数组长度为N。
1).比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个数据交换。
2).这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就“沉”到数组第N-1个位置。
3).N=N-1,如果N不为0就重复前面二步,否则排序完成。
常规实现:
void bubbleSort(int num[],int len){
for (int i=0; i<len; i++) {
bool flag = true;
for (int j=1; j<len-i; j++) {
if (num[j]<num[j-1]) {
swapNum(num[j-1], num[j]);
flag=false;
}
}
if (flag) {
break;
}
}
}
递归实现:
void bubbleSort(int *data,int start,int end)
{
if (start < end) {
int temp = 0;
int length = end - start + 1;
for (int i = start; i < length - 1; i ++) {
if (data[i] < data[i + 1]) {
temp = data[i];
data[i] = data[i + 1];
data[i + 1] = temp;
}
}
end --;
bubbleSort(data,start,end);
}
}
注意问题:end--那里,注意每次递归的起始终止下标的移动,起始下标不变,终止下标每次减1,循环结束条件 为start == end,以及i < length - 1;
3、 排序算法——直接插入排序
直接插入排序(Insertion Sort)的基本思想是:每次将一个待排序的记录,按其关键字大小插入到前面已经排好序的子序列中的适当位置,直到全部记录插入完成为止。
设数组为a[0…n-1]。
1). 初始时,a[0]自成1个有序区,无序区为a[1..n-1]。令i=1
2). 将a[i]并入当前的有序区a[0…i-1]中形成a[0…i]的有序区间。
3). i++并重复第二步直到i==n-1。排序完成。
常规实现:
void insertSort(int num[],int len){
for (int i=1; i<len; i++) {
if (num[i]<num[i-1]) {
int temp = num[i];
int j;
for (j=i-1;j>=0&&num[j]>temp; j--) {
num[j+1] = num[j];
}
num[j+1]=temp;
}
}
}
对将a[j]插入到前面a[0…j-1]的有序区间所用的方法进行改写,用数据交换代替数据后移。如果a[j]前一个数据a[j-1] > a[j],就交换a[j]和a[j-1],再j--直到a[j-1] <= a[j]。这样也可以实现将一个新数据新并入到有序区间。
void Insertsort(int a[], int n)
{
for (inti = 1; i < n; i++)
for (intj = i - 1; j >= 0 && a[j] > a[j + 1]; j--)
swapNum(a[j], a[j + 1]);
}
递归实现:
void insertationSort(int *data,int start,int end,int length)
{
if (end <= length){
int index = end;
int temp = 0;
//查找最后一个元素应该插入到哪个位置,找到则退出循环
for (int i = start; i < end; i ++) {
if (data[end] < data[i]) {
index = i;
temp = data[end];
break;
}
}
//如果找到指定位置了,则指定位置元素开始集体向后挪动一个单位,否则不变
if (index != end) {
data[index] = temp;
for (int i = end - 1; i >= index; i --) {
data[i + 1] = data[i];
}
}
end ++;
insertationSort(data, start, end, length);
}
}
4、 排序算法——希尔排序
希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。
该方法的基本思想是:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率上比前两种方法有较大提高。
以n=10的一个数组49, 38, 65, 97, 26, 13, 27, 49, 55, 4为例
第一次 gap = 10 / 2 = 5
49 38 65 97 26 13 27 49 55 4
1A 1B
2A 2B
3A 3B
4A 4B
5A 5B
1A,1B,2A,2B等为分组标记,数字相同的表示在同一组,大写字母表示是该组的第几个元素, 每次对同一组的数据进行直接插入排序。即分成了五组(49, 13) (38, 27) (65, 49) (97,55) (26, 4)这样每组排序后就变成了(13, 49) (27, 38) (49, 65) (55,97) (4, 26),下同。
第二次 gap = 5 / 2 = 2
排序后
13 27 49 55 4 49 38 65 97 26
1A 1B 1C 1D 1E
2A 2B 2C 2D 2E
第三次 gap = 2 / 2 = 1
4 26 13 27 38 49 49 55 97 65
1A 1B 1C 1D 1E 1F 1G 1H 1I 1J
第四次 gap = 1 / 2 = 0 排序完成得到数组:
4 13 26 27 38 49 49 55 65 97
void shellSort (int a[], int n){
for (int gap = n / 2; gap > 0; gap /= 2)
for (int j = gap; j < n; j++)//从数组第gap个元素开始
if (a[j] < a[j - gap]){//每个元素与自己组内的数据进行直接插入排序
int temp = a[j];
int k = j - gap;
while (k >= 0 && a[k] > temp) {
a[k + gap] = a[k];
k -= gap;
}
a[k + gap] = temp;
}
}
void shellSort(int num[], int len){
for (int gap=len/2; gap>0; gap=gap/2) {
for (int i=gap;i<len; i++) {
for (intj=i-gap;j>=0&&num[j]>num[j+gap];j=j-gap){
swapNum(num[j], num[j+gap]);
}
}
}
}
附注:上面希尔排序的步长选择都是从n/2开始,每次再减半,直到最后为1。其实也可以有另外的更高效的步长选择,如果读者有兴趣了解,请参阅维基百科上对希尔排序步长的说明:
http://zh.wikipedia.org/wiki/%E5%B8%8C%E5%B0%94%E6%8E%92%E5%BA%8F
5、排序算法——直接选择排序
直接选择排序和直接插入排序类似,都将数据分为有序区和无序区,所不同的是直接播放排序是将无序区的第一个元素直接插入到有序区以形成一个更大的有序区,而直接选择排序是从无序区选一个最小的元素直接放到有序区的最后。
设数组为a[0…n-1]。
1). 初始时,数组全为无序区为a[0..n-1]。令i=0
2). 在无序区a[i…n-1]中选取一个最小的元素,将其与a[i]交换。交换之后a[0…i]就形成了一个有序区。
3). i++并重复第二步直到i==n-1。排序完成。
常规实现:
void selectSort(int num[], int len){
for (int i=0; i<len; i++) {
int min = i;
for (int j=i+1; j<len; j++) {
if (num[j]<num[min]) {
min=j;
}
}
swapNum(num[i],num[min]);
}
}
递归实现:
void selectionSort(int *data,int start,int end)
{
if (start < end) {
//int length = end - start + 1;
int temp = data[start];
int index = start;
for (int i = start + 1; i < end + 1; i ++) {
if (data[index] > data[i]) {
index = i;
}
}
if (start != index) {
temp = data[start];
data[start] = data[index];
data[index] = temp;
}
start ++;
selectionSort(data, start, end);
}
}
注意问题:start ++那里,注意每次递归的起始终止下标的移动,起始每次加1,终止下标不变,
循环结束条件 为start == end,以及i < end + 1
6、 排序算法——归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
首先考虑下如何将将二个有序数列合并。这个非常简单,只要从比较二个数列的第一个数,谁小就先取谁,取了后就在对应数列中删除这个数。然后再进行比较,如果有数列为空,那直接将另一个数列的数据依次取出即可。
void combineNum(int num[], int start, int mid, int end, int temp[]){
int i=start,j=mid+1, k=0;
while (i<=mid &&j<=end){
if (num[i]<num[j]){
temp[k++]=num[i++];
}
else{
temp[k++]=num[j++];
}
}
while (i<=mid){
temp[k++]=num[i++];
}
while (j<=end){
temp[k++]=num[j++];
}
for (int i=0; i<k; i++) {
num[start+i]=temp[i];
}
}
void mergeSort(int num[], int start, int end, int temp[]){
if (start<end) {
int mid=start+(end-start)/2;
mergeSort(num, start, mid, temp);
mergeSort(num, mid+1, end, temp);
combineNum(num,start,mid,end,temp);
}
}
归并排序的效率是比较高的,设数列长为N,将数列分开成小数列一共要logN步,每步都是一个合并有序数列的过程,时间复杂度可以记为O(N),故一共为O(N*logN)。因为归并排序每次都是在相邻的数据中进行操作,所以归并排序在O(N*logN)的几种排序方法(快速排序,归并排序,希尔排序,堆排序)也是效率比较高的。
7、排序算法——快速排序
快速排序由于排序效率在同为O(N*logN)的几种排序方法中效率较高,因此经常被采用,再加上快速排序思想----分治法也确实实用,因此很多软件公司的笔试面试,包括像腾讯,微软等知名IT公司都喜欢考这个,还有大大小的程序方面的考试如软考,考研中也常常出现快速排序的身影。
总的说来,要直接默写出快速排序还是有一定难度的,因为本人就自己的理解对快速排序作了下白话解释,希望对大家理解有帮助,达到快速排序,快速搞定。
快速排序是C.R.A.Hoare于1962年提出的一种划分交换排序。它采用了一种分治的策略,通常称其为分治法(Divide-and-ConquerMethod)。
该方法的基本思想是:
1).先从数列中取出一个数作为基准数。
2).分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
3).再对左右区间重复第二步,直到各区间只有一个数。
虽然快速排序称为分治法,但分治法这三个字显然无法很好的概括快速排序的全部步骤。因此我的对快速排序作了进一步的说明:挖坑填数+分治法:
先来看实例吧,定义下面再给出(最好能用自己的话来总结定义,这样对实现代码会有帮助)。
以一个数组作为示例,取区间第一个数为基准数。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
72 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 48 | 85 |
初始时,i = 0; j = 9; X = a[i] = 72
由于已经将a[0]中的数保存到X中,可以理解成在数组a[0]上挖了个坑,可以将其它数据填充到这来。
从j开始向前找一个比X小或等于X的数。当j=8,符合条件,将a[8]挖出再填到上一个坑a[0]中。a[0]=a[8]; i++; 这样一个坑a[0]就被搞定了,但又形成了一个新坑a[8],这怎么办了?简单,再找数字来填a[8]这个坑。这次从i开始向后找一个大于X的数,当i=3,符合条件,将a[3]挖出再填到上一个坑中a[8]=a[3]; j--;
数组变为:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
72 | 6 | 57 | 88 | 60 | 42 | 83 | 73 | 88 | 85 |
i = 3; j = 7; X=72
再重复上面的步骤,先从后向前找,再从前向后找。
从j开始向前找,当j=5,符合条件,将a[5]挖出填到上一个坑中,a[3] = a[5]; i++;
从i开始向后找,当i=5时,由于i==j退出。
此时,i = j = 5,而a[5]刚好又是上次挖的坑,因此将X填入a[5]。
数组变为:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
48 | 6 | 57 | 42 | 60 | 72 | 83 | 73 | 88 | 85 |
可以看出a[5]前面的数字都小于它,a[5]后面的数字都大于它。因此再对a[0…4]和a[6…9]这二个子区间重复上述步骤就可以了。
对挖坑填数进行总结
1.i =L; j = R; 将基准数挖出形成第一个坑a[i]。
2.j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
3.i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
4.再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
void quickSort(int num[], int first, int last){
int start=first,end=last;
if (start < end) {
int pivot = num[start];
while (start<end) {
while (start<end &&pivot<=num[end]) {
end--;
}
if (start<end) {
num[start++]=num[end];
}
while (start<end &&num[start]<pivot) {
start++;
}
if (start<end) {
num[end--] = num[start];
}
}
num[start] = pivot;
quickSort(num, first, start-1);
quickSort(num, start+1,last);
}
}
8、排序算法——堆排序
堆排序与快速排序,归并排序一样都是时间复杂度为O(N*logN)的几种常见排序方法。学习堆排序前,先讲解下什么是数据结构中的二叉堆。
二叉堆的定义:二叉堆是完全二叉树或者是近似完全二叉树。
二叉堆满足二个特性:
1).父结点的键值总是大于或等于(小于或等于)任何一个子节点的键值。
2).每个结点的左子树和右子树都是一个二叉堆(都是最大堆或最小堆)。
当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。下图展示一个最小堆:
由于其它几种堆(二项式堆,斐波纳契堆等)用的较少,一般将二叉堆就简称为堆。
堆的存储
一般都用数组来表示堆,i结点的父结点下标就为(i – 1) / 2。它的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。
堆的操作——插入删除
下面先给出《数据结构C++语言描述》中最小堆的建立插入删除的图解,再给出本人的实现代码,最好是先看明白图后再去看代码。
堆的插入
每次插入都是将新数据放在数组最后。可以发现从这个新数据的父结点到根结点必然为一个有序的数列,现在的任务是将这个新数据插入到这个有序数据中——这就类似于直接插入排序中将一个数据并入到有序区间中。
//最小堆
void adjustHeap(int num[], int parent,int len){
int temp=num[parent];
while(2*parent+1<=len) {
int child=2*parent+1;
if(child<len&&num[child]>num[child+1]){
child++;
}
if (num[child]<temp) {
num[parent]=num[child];
}
else{
break;
}
parent=child;
}
num[parent]=temp;
}
void heapSort(int num[],int len){
for (int i=(len>>1)-1; i>=0; i--) {
adjustHeap(num,i,len-1);
}
for (int i=len-1; i>=0; i--) {
swapNum(num[0], num[i]);
adjustHeap(num,0,i-1);
}
}