1.冒泡排序
冒泡排序的核心思想就是比较一维数组中相邻位置上的两个值,例如,最终的目的是得到一个升序排列的序列,那么两个位置上的值,A0>A1,则交换位置,否则不交换。例如对下面的数组进行排序:
3 | 0 | 4 | 5 | 1 | 7 | 6 | 2 |
第一趟冒泡过程如下:
第一次,3>0,那么互换位置:
0 | 3 | 4 | 5 | 1 | 7 | 6 | 2 |
第二次,3<4,那么不换位置。
0 | 3 | 4 | 5 | 1 | 7 | 6 | 2 |
第三次,4<5,不交换位置:
0 | 3 | 4 | 5 | 1 | 7 | 6 | 2 |
第四次,5>1,互换位置:
0 | 3 | 4 | 1 | 5 | 7 | 6 | 2 |
第五次,5<7,不交换位置:
0 | 3 | 4 | 1 | 5 | 7 | 6 | 2 |
第六次,7>6,互换位置:
0 | 3 | 4 | 1 | 5 | 6 | 7 | 2 |
第七次,7>2,互换位置:
0 | 3 | 4 | 1 | 5 | 6 | 2 | 7 |
这样经过第一轮冒泡之后,将最大的数字排在了最大索引的地方上。
具体实现,就是通过两轮循环之后得到一个按照升序排列的序列。代码实现如下:
#include <iostream> #include <string> using namespace std; //所以这个int * 提示我们int * 不一定是指针,也可能是个数组 void bubbleSort(int *pUnsortedArray,int iElementNum) { //外层循环决定要冒泡多少次 for(int i = 0; i < iElementNum-1; ++i) { //内层循环负责具体的冒泡过程 //循环的终止条件是iElementNum-i-1,因为每i趟冒泡就已经有i个元素的位置被安排好了 for(int j = 0; j < iElementNum-i-1;++j) { if(pUnsortedArray[j] > pUnsortedArray[j+1]) { //如果相邻位置上的元素前一个大于后一个,那么则交换位置 int iTemp = pUnsortedArray[j]; pUnsortedArray[j] = pUnsortedArray[j+1]; pUnsortedArray[j+1] = iTemp; } } } } int main(int argc,char *argv[]) { int iArray[8] = {3,0,4,5,1,7,6,2}; bubbleSort(iArray,8); for(int i =0 ; i < 8 ; ++i) { cout << iArray[i] << std::endl; } return (0); }
按照上述代码,我们需要经过n-1 + n-2 + n-3 + ... + 1 = n(n-1)/2次可以得到一个有序序列,所以冒泡排序算法的时间复杂度是O(n^2),空间复杂度是O(1)。
2.简单选择排序
选择排序的思想和冒泡排序有相同之处。就是找到最小元素,记录下它的位置,并和剩下序列的第一个元素交换位置。每一次循环都找到一个最小元素。
3 | 0 | 4 | 5 | 1 | 7 | 6 | 2 |
演示一趟完整的选择排序过程,这也是区别于冒泡排序的不同之处。
初始情况下,我们认为索引0处的元素就是最小的。
第一次比较3>0,那么显然索引0处的元素已经不是最小的了,我们记录下这个索引1。
第二次比较用最小元素和还未经过比较的其余元素进行比较,此处是用索引1的值和索引2的值进行比较,发现0<4,那么此时,索引1的值依然是最小的。最小索引不用更新。
第三次比较,此时索引3、4、5、6、7还未进行比较。那最小索引和3索引比较,发现索引1处的值还是最小的,那么不用更新索引。
重复这个比较的过程,知道8个索引对应的值都被比较过了。此时我们得到最小值存储在了索引1处,然后将索引1和索引0的值进行交换,此时第一趟结束。
第二趟将在剩下的7个元素中进行。
代码实现:
#include <string> #include <iostream> using namespace std; //最终的结果是,我要得到一个升序排列的序列 void selectionSort(int *unsortedElement,int iElementNum) { for(int i = 0; i < iElementNum; ++i) { int iIndexOfMinalElement = i; //每一趟循环时,最小元素的索引都初始化为剩余未排序元素中的第一个位置 for(int j= i; j < iElementNum;++j) { if( unsortedElement[iIndexOfMinalElement] > unsortedElement[j]) { //如果当前记录下的最小元素比当前索引处的值还要大,那么将最小值的索引更新为当前索引 iIndexOfMinalElement = j; } } int iTemp = unsortedElement[i]; unsortedElement[i] = unsortedElement[iIndexOfMinalElement]; unsortedElement[iIndexOfMinalElement] = iTemp; } }
选择排序是对冒泡排序的一种改进,对比两个算法可以发现,冒泡排序的无论交换还是比较,时间复杂度都是O(n^2),而选择排序,它从比较的角度来看,时间复杂度是O(n^2),而移动的次数为O(n),但是移动的时间消耗是要大于交换的时间消耗的,因为移动涉及到了和内存进行数据的交换。所以从这个角度来讲选择排序的性能是要优于冒泡排序的。
3.直接插入排序算法(还有另外一种折半插入算法)
直接插入算法的核心思想是在未排序的序列中,和已经排好序的序列中的元素,进行比较,将其插入适当的位置。插入排序还有一个点就是要把一个完整的未排序的序列分为两个部分,一部分是未经过排序的部分,另一部分是已经经过排序的部分。
4 | 3 | 1 | 2 |
看这4个元素完整的排序过程,
首先我们假定这个序列的第一个索引的处的值(也就是4)是排好序的。
第一趟:
第一步:首先取出未排序序列的第一个元素3,得到
4 | 1 | 2 |
第二步:和已经排序的元素进行比较,找到它应该插入的位置,在这个例子中,4>3,所以从4这个位置开始(包括4),所有已经排好序的元素依次向后移动一个元素的位置,得到:
4 | 1 | 2 |
第三步,将3插入适当的位置:
3 | 4 | 1 | 2 |
这样一来,我们就得到了两个已经排好序的元素3和4,还剩下两个未排序的元素:1和2,对1其进行和第一趟排序过程一模一样的操作,最终就能得到一个升序排列的序列了。
代码实现:
#include <string> #include <iostream> using namespace std;
void insertionSort(int *pArrayElement,const int iElementNum)
{
//在首次排序的时候,假定这个未排序的序列中第一个元素是已经排好序的,所以循环的初始条件是i=1
//插入排序的另一个要点是,要把整个序列分为两部分,一部分是经过排序的部分,还有一部分是未经过排序的部分
//外层循环控制的是未排序的元素部分
for(int i = 1;i < iElementNum;++i)
{
//在进行插入操作之前,始终从未排序的部分拿出第一个元素和已经排好序的部分中的每一个元素进行比较,然后将它插入合适的位置
//i以前的元素是已经排好序的元素,i以后(包括i)以后是未排序的元素
//所以如果说未排序部分的第一个元素要比排好序部分的最后一个元素小,那么我们就要为它在已经排好序的部分找到一个合适的位置插入这个值
int j = i-1;
//首先要保存未排好序部分的首元素,因为接下来我们要开始移动整个序列中的元素
int iUnsortedElement = pArrayElement[i];
while(j>=0 && iUnsortedElement < pArrayElement[j])
{
//移位
pArrayElement[j+1] = pArrayElement[j];
--j;
}
//完成最后一步的插入工作
pArrayElement[j+1] = iUnsortedElement;
}
}
插入排序,移动次数是n(n-1)/2,比较次数也是n(n-1)/2,所以综合来看它的时间复杂度是O(n^2)
4.希尔排序
久仰大名了。在希尔排序之前的排序算法时间复杂度都是O(n^2),希尔排序算法是第一批突破这个时间复杂度的算法之一。
思路是将原本有大量记录数的记录进行分组,分割成若干个子序列,这样子序列的个数就比较少了,然后在这些子序列内部进行直接插入操排序,当整个序列基本有序时,再对全体记录做一次直接插入排序,所谓基本有序就是小的关键字基本在前面,大的基本在侯曼,不大不小的基本在中间,例如{2,1,3,6,4,7,5,8,9}这样就可以称为基本有序了,而{1,5,9,3,7,8,2,4,6}就不能称为基本有序的。
为了能让全体记录达到基本有序,需要采取跳跃分割的策略:将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
待排序的序列是:
0 | 9 | 1 | 5 | 8 | 3 | 7 | 4 | 6 | 2 |
希尔排序的关键之处在于怎么选取这个增量,一个建议是首次进行排序时将增量选为待排序列长度的一半,第二次将增量减半,以此类推,直至将这个增量减少到1为止。来演示一下第一趟排序的过程:
总共有10个元素,那么第一趟的增量就是5,我们先将这个序列进行以5位增量切分为几个子序列(这些子序列在编码的时候不一定真实存在,这里只是为了描述方便):
从索引0处开始,以5为增量,得到的第一个子序列是(索引0,5)
0 | 3 |
索引1开始,以5为增量,得到的第二个子序列是(索引1,6)
9 | 7 |
索引2开始,以5为增量,得到的第二个子序列是(索引2,7)
1 | 4 |
索引3开始,以5为增量,得到的第二个子序列是(索引3,8)
5 | 6 |
索引4开始,以5为增量,得到的第二个子序列是(索引4,9)
8 | 2 |
对上面切分得到的这些个子序列分别进行直接插入排序,例如对0,3进行直接插入排序得到的结果就是0,3,最后将这5个子序列直接插入排序的结果整合在一起,最终的结果是{0,3,7,9,1,4,5,6,2,8}。
重复上面的步骤,当增量变到1时得到排序结果就是我们最终要的有序序列了。
代码实现:
void shellSort(int *pUnsortedArray,const int iElementNum) { int iStep = iElementNum; //增量的初始值 //最外面这一层循环控制的是每一趟排序过程,增量是多少 do { iStep /= 2; //切割完整的序列,并对子序列进行插入排序 //对于每一个子序列而言,都假定首元素是有序的 //那么待排序部分的第一个元素的索引就是 iStep; for(int i = iStep; i < iElementNum; ++i ) { //如果未排序部分的第一个元素要比已经排好序的最后一个元素大,那么就要在已经排好序的部分找到一个合适的位置,并将这个新加入的元素进行排序 //在做插入排序的时候,倒序进行插入 int iUnsortedElement = pUnsortedArray[i]; //将首个没有排序的元素保存下来 int j = i - iStep; int iIndex = 0; while(j >=0 && iUnsortedElement < pUnsortedArray[j]) { //现在最内层的循环就是已经排好序的部分了,我们为待插入元素找到一个合适的插入位置 //这里开始移动位置 pUnsortedArray[j+iStep] = pUnsortedArray[j]; j -= iStep; } //经过最内层循环的操作之后,已经把待插入位置空了出来,现在我们把这个没有排好序的元素插入到空出来的位置中 //最后一次循环的时候j有一个减操作,所以实际上我们要插入的位置其实是j+iStep pUnsortedArray[j+iStep] = iUnsortedElement; } //std::cout << "循环控制变量:" << i << std::endl; } while(iStep >1); //循环终止的条件是iStep > 1,因为当iStep 为2的时候我们在循环体里面就已经得到了最后的增量iStep = 1 }
希尔排序最差情况下,时间复杂度是O(n^2),最好情况(元素已经有序)时间复杂度是O(n^1.3)
5.归并排序
归并排序是分而治之思想的经典案例。它的核心过程分为两步,一是分的过程:就是把一个完整的待排序数组,分成一个个只有单个元素的子数组(逻辑上的划分,并不是将划分结果存储在单独的数组里),那么这样一来每一个子数组就都是有序的。第二步,就是将得到的子数组进行归并排序。例如:
0 | 1 | 2 | 3 |
最后将它分成只有一个元素的子数组(注意这只是逻辑上的一种划分):
子数组:{0},{1},{2},{3}。
接下来,进行一个归并排序的过程,{0,1},{2,3}
最后一次归并后得{0,1,2,3}
代码实现:
//归并排序算法实现 //归并排序是典型的分而治之的思想产物。它分为两个核心的步骤,一个就是将待排序的序列拆分成只包含一个元素的子序列,另一个步骤就是对拆分结果进行合并。 //对已经有序的子数组进行归并排序:记住两个前提,首先归并的两个子数组本身是有序的,其次,“分”的步骤,得到的子数组并不是存储在一个新的空间里,它还是在原来的空间中,只不过,我们在逻辑上把它进>行了划分 #include <memory> #include <string> #include <iostream> using namespace std; void merge(int iUnsortedArray[], const int iLeft, const int iMid, const int iRight) { //整个归并的过程,时间复杂度为O(n) //注意一下,这个iRight参数传进来的并不是数组的最大索引,而是最大索引+1 //int *pTempResult = new int [iRight - iLeft]; //申请一块内存,用于存放临时的排序结果,shared_ptr不直接支持对动态数组的管理,所以需要单独提供一个deleter std::shared_ptr<int> shpITempResult(new int[iRight - iLeft], [](int *p) { if (nullptr != p) { delete[]p; p = nullptr; } }); int iLeftStartIndex = iLeft; //左半子数组的开始索引 int iRightStartIndex = iMid; //右半子数组的开始索引 int iResultStartIndex = 0; //临时数组的开始索引 while (iLeftStartIndex < iMid || iRightStartIndex < iRight) { if (iLeftStartIndex >= iMid) { //出现这种情况说明右半边的子数组要比左半边的子数组长,那么左半边的元素已经全都插入到了临时数组中,剩下的事情就是把右半边的元素追加到临时结果数组中即可 shpITempResult.get()[iResultStartIndex++] = iUnsortedArray[iRightStartIndex++]; } else if (iRightStartIndex >= iRight) { //出现这种情况,那说明左半边的元素数量比右半边要多,那么右半边的元素已经全部插入到了临时结果数组中,剩下的事情就是把左半边剩余的元素追加到临时结果数组中即可 shpITempResult.get()[iResultStartIndex++] = iUnsortedArray[iLeftStartIndex++]; } else { //任何一边的数组都没有被排完 if (iUnsortedArray[iLeftStartIndex] < iUnsortedArray[iRightStartIndex]) { shpITempResult.get()[iResultStartIndex++] = iUnsortedArray[iLeftStartIndex++]; } else { shpITempResult.get()[iResultStartIndex++] = iUnsortedArray[iRightStartIndex++]; } } } iResultStartIndex = 0; for (int i = iLeft; i < iRight; ++i) { iUnsortedArray[i] = shpITempResult.get()[iResultStartIndex++]; } //if(pTempResult != nullptr) //{ //delete []pTempResult; //pTempResult = nullptr; //} } //iUnsortedArray是待排序的数组,iLeft是左端的索引起始值,在调用时,它的初始值为0,iRight,在首次调用时,它的初始值是待排序数组的长度 void mergeSort(int iUnsortedArray[], const int iLeft, const int iRight) { //最终的目标是将待排序的数组在逻辑上把它划分为只有一个元素子数组 if (iLeft + 1 < iRight) { int iMid = (iRight + iLeft) / 2; mergeSort(iUnsortedArray, iLeft, iMid); mergeSort(iUnsortedArray, iMid, iRight); merge(iUnsortedArray, iLeft, iMid, iRight); } } int main(int argc, char *argv[]) { int iUnsortedArray[] = { 9,13,1,14,0,12,2,11,3,10,4,8,5,7,6 }; std::cout << "Before sorting:"; for (int i = 0; i < 15; ++i) { std::cout << iUnsortedArray[i] << " "; } std::cout << std::endl; mergeSort(iUnsortedArray, 0, 15); std::cout << "After Sorting:"; for (int i = 0; i < 15; ++i) { std::cout << iUnsortedArray[i] << " "; } std::cout << std::endl; system("pause"); return (0); }
归并算法的时间复杂度是O(nlogn)。