2018年5月7日上午十点整,本宝宝决定从头开始复习,每天一半的时间复习基础知识,一半的时间做项目(多实践)看了第一本大话数据结构,以后想把每一本书比较重要的知识点总结一下。
排序算法的稳定性:假设Ki=Kj (1<=i<=n,1<=j<=n,i!=j),且在排序前的序列中ri领先于rj,如果排序后ri仍领先rj,则称所用的排序是稳定的,如果改变了,就不是稳定的。
1. 冒泡排序
【1】冒泡排序的思想:
两两比较相邻的关键字,如果反序则交换,知道没有反序的记录为止。
冒泡排序算法的运作如下:
(1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这 步做完后,最后的元素会是最大的数。
(3)针对所有的元素重复以上的步骤,除了最后一个。
(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
【2】冒泡排序的初级版
void bubblesort(int *a,int n)
{
for(int i=0;i<n;i++)
{
for(int j=i+1;j<n;j++)
{
if(a[j]<a[i])
{
swap(a[j],a[i]);
}
}
}
}
【3】真正的冒泡排序
void bubblesort(int *a,int n)
{
for(int i=0;i<n;i++)
{
for(int j=n-2;j>=i;j--)
{
if(a[j]>a[j+1])
{
swap(a[j],a[j+1]);
}
}
}
}
【4】冒泡排序的优化
void bubblesort(int *a,int n)
{
bool flag=true;
for(int i=0;i<n&&flag==true;i++)
{
flag=false;
for(int j=n-2;j>=i;j++)
{
if(a[j]>a[j+1])
{
swap(a[j],a[j+1]);
flag=true;
}
}
}
}
冒泡排序最好的情况也就是排序本身有序的,我们会比较n-1次,没有数据交换,所以时间复杂度为O(n),最坏的情况下是数据是逆序的,此时要比较n(n-1)/2次,所以时间复杂度为O(n*n)*,空间复杂度为O(1),冒泡排序是稳定的排序。
2. 选择排序
选择排序的思想:
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
void selectsort(int *a,int n)
{
int min;
for(int i=0;i<n;i++)
{
min=i;
for(int j=i+1;j<n;j++)
{
if(a[min]>a[j])
{
min=j;
}
if(min!=i)
{
swap(a[i],a[min]);
}
}
}
}
选择排序的主要优点与数据移动有关。如果某个元素位于正确的最终位置上,则它不会被移动。选择排序每次交换一对元素,它们当中至少有一个将被移到其最终位置上,因此对 n个元素的表进行排序总共进行至多n-1次交换。在所有的完全依靠交换去移动元素的排序方法中,选择排序属于非常好的一种。选择排序的时间复杂度为O(n*n),空间复杂度为O(1),选择排序是不稳定的排序。
3. 插入排序
插入排序的思想:
通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。(将一个记录,插入到已经排好序的有序表中,从而得到一个新的,记录数增1的有序表)
void insertsort(int *a,int n)
{
int temp,j;
for(int i=1;i<n;i++)
{
if(a[i]<a[i-1])
{
temp=a[i];
for(j=i-1;a[j]>temp&&j>=0;j--)
{
a[j+1]=a[j];
}
a[j+1]=temp;
}
}
}
插入排序最好的情况是有序的时候,时间复杂度为O(n),最坏的情况时间复杂度为O(n*n),空间复杂度为O(1),插入排序是稳定的排序。
4. 希尔排序
基本有序:
基本有序是指小的关键字基本在前面,大的关键字基本在后面,不大不小的基本在中间。
我们要采用跳跃分割的策略:
将相距某个增量的记录组成一个子序列,这样才能保证在子序列内进行直接插入排序后得到的结果是基本有序的而不是局部有序的。(增量一般increment=increment/2,向下取整)
希尔排序的主要思想:
利用增量进行分割,使其数据变成基本有序,然后再利用插入排序,将其变成有序。
void shellsort(int *a,int n)
{
int i,j,temp;
int increment=n;
do
{
increment=increment/2;
for(i=increment;i<n;i++)
{
if(a[i]<a[i-inctement])
{
temp=a[i];
for(j=i-increment;j>=0&&temp<a[j];j=j-increment)
{
a[j+increment]=a[j];
}
a[j+increment]=temp;
}
}
}
while(increment>1);
}
希尔排序的时间复杂度为O(n*n),他的空间复杂度为O(1),希尔排序是不稳定的排序。
5. 堆排序
堆排序的思想:
堆排序就是利用堆(假如利用大顶堆)进行排序的方法。他主要是,将待排序的序列构成一个大顶堆。此时整个序列的最大值就是堆顶的根结点。将他移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次大值,反复执行,就能得到一个有序的序列。
堆排序处理的两个主要步骤:
(1)如何由一个无序序列构建成一个堆
(2)如何在输出堆顶元素后,调整剩余元素成为一个新的堆
void heapsort(int *a,int n)
{
for(int i=(n-1)/2;i>=0;i--)
{
heapadjust(a,i,n);
}
for(int i=n-1;i>=0;i--)
{
swap(a[0],a[i]);
heapadjust (a,0,i);
}
}
void heapadjust(int *a,int i,int n)
{
int leftindex=2*i+1;
int rightindex=2*i+2;
int max=i;
if(i<=(n-1)/2)
{
if(leftindex<n&&a[leftindex]>a[max])
{
max=leftindex;
}
if(rightindex<n&&a[rightindex]>a[max])
{
max=rightindex;
}
if(max!=i)
{
swap(a[i],a[max]);
heapadjust(a,max,n);
}
}
}
堆排序的运行时间主要消耗在初始构建堆和在重建堆时的反复筛选上。
在构建堆过程中,因为我们是完全二叉树从最下层最右边的结点开始构建,将它与其孩子进行比较和若有必要的交换,对每个非终端节点来说,其实最多进行两次互换操作,因此整个构建堆的时间复杂度为O(n)
在正式排序时,第i次取堆顶记录重建堆需要O(log n),因此重建堆的时间复杂度为O(n*log n)
堆排序的最好,最坏,平均时间复杂度都为O(n*log n),空间复杂度为O(1),堆排序是不稳定的排序。
由于初始建堆所需比较的次数较多,因此,它并不适合待排序的序列个数较少的情况
6. 归并排序
归并排序的思想:
假设初始序列有n个元素,则可以看成n个有序的子序列,整个子序列的长度为1,然后两两归并,如此重复下去,直到得到一个长度为n的有序序列为止。
void mergesort(int *a,int n)
{
int *p=new int[n];
MergeSort(a,0,n-1,p);
delete[] p;
}
void MergeSort(int *a,int first,int last,int *temp)
{
if(first<last)
{
int mid=(first+last)/2;
MergeSort(a,first,mid,temp);//左边有序
MergeSotr(a,mid+1,last,temp);//右边有序
mergeArray(a,first,mid,last,temp);//将两个序列合并
}
}
void mergeArray(int *a,int first,int mid,int last,int *temp)
{
int i=first;
int j=mid+1;
int p=mid;
int q=last;
int k=0;
while(i<=p&&j<=q)
{
if(a[i]<=a[j])
{
temp[k++]=a[i++];
}
else
{
temp[k++]=a[j++];
}
while(i<=p)
{
temp[k++]=a[i++];
}
while(j<=q)
{
temp[k++]=a[j++];
}
for(int i=0;i<k;i++)
{
a[first+i]=temp[i];
}
}
}
归并排序最好,最坏,平均时间复杂度为O(n*log n),归并排序的空间复杂度为O(n),归并排序是稳定的排序。
归并排序是一种比较占用内存,但却效率高且稳定的算法。
7. 快速排序
快排的思想:
(1)从数据中选出一个元素作为基准
(2)重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。
(3)递归的重复对基准两边的数列做(1)和(2)操作,直到是一个有序的序列为止
void QuickSort(int *a,int n)
{
quickSort(a,0,n-1);
}
void quickSort(int *a,int first,int last)
{
int part;
if(first<last)
{
part=partion(a,first,last);
quickSort(a,first,part-1);
quickSort(a,part+1,last);
}
}
int partion(int *a,int first,int last)
{
int val=a[first];
while(first<last)
{
while(first<last&&a[last]>=val)
{
last--;
}
swap(a[first],a[last]);
while(first<last&&a[first]<=val)
{
first++;
}
swap(a[first],a[last]);
}
return first;
}
快速排序在最优的情况下时间复杂度为O(n*log n),快速排序最坏的情况下是待排序的序列是正序或者逆序,其时间复杂度为O(n*n),平均时间复杂度为O(n*log n),快速排序的空间复杂度,在最好的情况下他的空间复杂度为O(log n),在最坏的情况下,它的空间复杂度为O(n),平均情况,空间复杂度为O(log n),快速排序是不稳定的排序。
三种快排,四种优化:
方法(1):固定位置
思想:取序列的第一个或最后一个元素作为基准(注意:基本的快速排序选取第一个或最后一个元素作为基准)
如果输入序列是随机的,处理时间可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。因为每次划分只能使待排序序列减一,此时为最坏情况,快速排序沦为起泡排序,时间复杂度为Θ(n^2)。而且,输入的数据是有序或部分有序的情况是相当常见的。因此,使用第一个元素作为枢纽元是非常糟糕的,为了避免这个情况,就引入了下面两个获取基准的方法。
int SelectPivot(int arr[],int low,int high)
{
return arr[low];//选择选取序列的第一个元素作为基准
}
方法(2):随机选取基准
引入的原因:在待排序列是部分有序时,固定选取枢轴使快排效率底下,要缓解这种情况,就引入了随机选取枢轴
思想:取待排序列中任意一个元素作为基准
这是一种相对安全的策略。由于枢轴的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)。
/*随机选择枢轴的位置,区间在low和high之间*/
int SelectPivotRandom(int arr[],int low,int high)
{
//产生枢轴的位置
srand((unsigned)time(NULL));
int pivotPos = rand()%(high - low) + low;
//把枢轴位置的元素和low位置元素互换,此时可以和普通的快排一样调用划分函数
swap(arr[pivotPos],arr[low]);
return arr[low];
}
方法(3):三数取中
引入的原因:虽然随机选取枢轴时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取枢轴
思想:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为枢纽元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为枢纽元。显然使用三数中值分割法消除了预排序输入的不好情形
举例分析:
待排序序列为:9 3 8 2 0 710
左边:9 中间:2 右边:10
将三个数排序后9位于中间,所以最终选取的基准为9
/*函数作用:取待排序序列中low、mid、high三个位置上数据,选取他们中间的那个数据作为枢轴*/
int SelectPivotMedianOfThree(int arr[],int low,int high)
{
int mid = low + ((high - low) >> 1);//计算数组中间的元素的下标
//使用三数取中法选择枢轴
if (arr[mid] > arr[high])//目标: arr[mid] <= arr[high]
{
swap(arr[mid],arr[high]);
}
if (arr[low] > arr[high])//目标: arr[low] <= arr[high]
{
swap(arr[low],arr[high]);
}
if (arr[mid] > arr[low]) //目标: arr[low] >= arr[mid]
{
swap(arr[mid],arr[low]);
}
//此时,arr[mid] <= arr[low] <= arr[high]
return arr[low];
//low的位置上保存这三个位置中间的值
//分割时可以直接使用low位置的元素作为枢轴,而不用改变分割函数了
}
快速排序的四种优化方式:
优化1、当待排序序列的长度分割到一定大小后,使用插入排序。
原因:对于很小和部分有序的数组,快排不如插排好。当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差,此时可以使用插排而不是快排。
截止范围:待排序序列长度N = 10,虽然在5~20之间任一截止范围都有可能产生类似的结果,这种做法也避免了一些有害的退化情形。
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}//else时,正常执行快排
优化2、在一次分割结束后,可以把与Key相等的元素聚在一起,继续下次分割时,不用再对与key相等元素分割
具体过程:在处理过程中,会有两个步骤
第一步,在划分过程中,把与key相等元素放入数组的两端
第二步,划分结束后,把与key相等的元素移到枢轴周围
void QSort(int arr[],int low,int high)
{
int first = low;
int last = high;
int left = low;
int right = high;
int leftLen = 0;
int rightLen = 0;
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}
//一次分割
int key = SelectPivotMedianOfThree(arr,low,high);//使用三数取中法选择枢轴
while(low < high)
{
while(high > low && arr[high] >= key)
{
if (arr[high] == key)//处理相等元素
{
swap(arr[right],arr[high]);
right--;
rightLen++;
}
high--;
}
arr[low] = arr[high];
while(high > low && arr[low] <= key)
{
if (arr[low] == key)
{
swap(arr[left],arr[low]);
left++;
leftLen++;
}
low++;
}
arr[high] = arr[low];
}
arr[low] = key;
//一次快排结束
//把与枢轴key相同的元素移到枢轴最终位置周围
int i = low - 1;
int j = first;
while(j < left && arr[i] != key)
{
swap(arr[i],arr[j]);
i--;
j++;
}
i = low + 1;
j = last;
while(j > right && arr[i] != key)
{
swap(arr[i],arr[j]);
i++;
j--;
}
QSort(arr,first,low - 1 - leftLen);
QSort(arr,low + 1 + rightLen,last);
}
优化3:优化递归操作
快排函数在函数尾部有两次递归操作,我们可以对其使用尾递归优化
优点:如果待排序的序列划分极端不平衡,递归的深度将趋近于n,而栈的大小是很有限的,每次递归调用都会耗费一定的栈空间,函数的参数越多,每次递归耗费的空间也越多。优化后,可以缩减堆栈深度,由原来的O(n)缩减为O(logn),将会提高性能。
void QSort(int arr[],int low,int high)
{
int pivotPos = -1;
if (high - low + 1 < 10)
{
InsertSort(arr,low,high);
return;
}
while(low < high)
{
pivotPos = Partition(arr,low,high);
QSort(arr,low,pivot-1);
low = pivot + 1;
}
}
优化4:使用并行或多线程处理子序列
注:快速排序的空间复杂度是图片上面的分析,归并排序的空间复杂度也参考上面的分析
参考:(1)https://blog.csdn.net/cpcpcp123/article/details/52739285 (2)https://zh.wikipedia.org/wiki/%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95
(3)《大话数据结构》