将常见的排序算法进行复现,加以简要分析:
1. 冒泡排序
主要思想:对比相邻两个元素的大小,如果不符合预设要求,就交换位置,每一遍循环都将当前循环范围内的最大值移动到该循环范围的末尾位置
注意事项:
(1)循环时将当前元素与该元素下一位置的元素进行比较,循环的范围为[0,iSize-1)
(2)由于每次循环,都将最大值移至循环范围的末尾位置,所以第二层循环的范围为[0,iSize-1-i)
代码实现:
void BubbleSort(vector<int>& vSort)
{
int iSize = vSort.size();
for (int i = 0; i < (int)iSize-1;i++)
{
#从头开始遍历,保证循环范围内最大值被移至当前范围末尾
for (int j = 0; j < (int)iSize-1-i; j++)
{
if (vSort[j] > vSort[j + 1])
{
swap(vSort[j], vSort[j + 1]);
}
}
}
return;
}
2. 选择排序
主要思想:每一次循环都找到当前循环范围内的最大元素,记录下标,并在该循环完成后将循环范围末尾下标的元素与该“最大”元素交换位置
注意事项:
(1)记录当前循环范围内“最大”元素下标的变量iTemp应定义在第一层循环中
(2)第二层循环的循环范围是[1,iSize-i),注意iTemp=0
(3)进行元素交换时swap(vSort[iTemp], vSort[iSize - i - 1]),注意末尾元素的下标要减去1,防止越界
代码实现:
void SelectSort(vector<int>& vSort)
{
int iSize = vSort.size();
for (int i = 0; i < (int)iSize; i++)
{
int iTemp = 0; //存储当前最大值下标
for (int j = 1; j < (int)iSize - i; j++)
{
if (vSort[j] > vSort[iTemp])
{
iTemp = j;
}
}
swap(vSort[iTemp], vSort[iSize - i - 1]); //将当前循环范围最大值移至范围末尾
}
return;
}
3.插入排序
主要思想:将某一位置的元素与该位置之前已排序完成的元素进行一一对比,找到比该元素大的下标后,将该元素插入,其他元素后移一位
注意事项:
(1)第一层循环可以直接从第二个元素开始
(2)找到插入位置后,依次与该元素对应下标进行交换即可,无需再创建新循环,效果相同
代码实现:
void InsertSort(vector<int>& vSort)
{
int iSize = vSort.size();
for (int i = 1; i < (int)iSize; i++) //从第二个元素开始遍历,首元素自己可看做已排序完成
{
for (int j = 0; j < i; j++) //从首部遍历至该元素前一位
{
if (vSort[j] <= vSort[i])
{
continue;
}
else //找到插入位置,进行元素位置交换
{
for (; j < i; j++)
{
swap(vSort[j], vSort[i]);
}
}
}
}
return;
}
注意事项:
(1)第一层循环可以直接从第二个元素开始
(2)第二层循环从已排序元素的尾部开始,注意第二层的下标要在循环外定义
(3)循环结束后再放置该元素,否则最小的元素将被覆盖,因为在循环内部 j+1 永远 >=1(!=0)
代码实现
void InsertSort(vector<int>& vSort)
{
int iSize = vSort.size();
for (int i = 1; i < (int)iSize; i++) //从第二个元素开始遍历,首元素自己可看做已排序完成
{
int iTemp = vSort[i];
int j = i - 1; //从排序元素的尾部开始遍历,寻找插入位置
for (; j >= 0; j--)
{
if (vSort[j] > iTemp) //比该元素大,则后移一位
{
vSort[j + 1] = vSort[j];
}
else
{
break;
}
}
vSort[j + 1] = iTemp; //在比该元素小的元素的后一位放置
}
return;
}
4.希尔排序
主要思想:设置一个动态变化的分隔值,保证规律变化,最后可以变为1即可(通常>>1);每次排序按照分隔值将数组分割成多份,每一份按照插入排序的思想进行排列,
注意事项:
(1)第一层循环,保证分隔最小为1
(2)第二层循环,从各个分隔组的第二个值开始,与分组内所有的前项值进行对比
(3)第三层循环,在不发生越界的前提下,如果当前值小于前项值,将前项值后移,直到无法进入循环,此时将当前值正确放置
(4)第三层循环相当于插入排序的第二层循环,应注意到while循环和for循环的区别,也就是j值是否可以直接使用
代码实现:
void ShellSort(vector<int>& vSort)
{
int iSize = vSort.size();
int iStep = iSize >> 1;
while (iStep)//保证分组间距>=1
{
for (int i = iStep; i < iSize; i++)//开始遍历
{
int j = i;
int iTemp = vSort[i];
while (j - iStep >= 0 && iTemp < vSort[j - iStep]) //将当前值与间隔分组后,组内前边的值进行一一对比
{ //当前值前边的值都已经排序完成,只需要与排序完成的末尾值进行对比即可知道是否需要进入循环
//swap(vSort[j-iStep], vSort[j]);//无需每次都进行交换,可以将较大值后移
vSort[j] = vSort[j - iStep];
j -= iStep;
}
vSort[j] = iTemp;
}
iStep = iStep >> 1;
}
return;
}
5.归并排序
主要思想:将数组不断分割成大小为1或2的小数组时,再逆着分割顺序进行排序,首先将每一个小数组排序完成,再讲相邻数组成对进行排序,此时问题可看做两个已排序好的数组的合并问题
注意事项:
(1)定义各数组的iMid时不要忘记加上iBeg
(2)进行合并时必须要创建临时数组
(3)比较完成后,注意将未操作的数值直接放在临时数组末尾即可
(4)将临时数组拷贝至源数组时应注意两数组的起点不同
代码实现:
void MergeSort(vector<int>& vSort, int iBeg, int iEnd)
{
int iMid = iBeg + (iEnd - iBeg) / 2;//在iBeg基础上得到iMid
if (iEnd - iBeg > 1)
{
MergeSort(vSort, iBeg, iMid);
MergeSort(vSort, iMid + 1, iEnd);
}
vector<int> vTemp;//创建临时数组作为存储
int i = iBeg, j = iMid + 1;
while (i <= iMid && j <= iEnd)
{
if (vSort[i] <= vSort[j])
{
vTemp.push_back(vSort[i++]);
}
else
{
vTemp.push_back(vSort[j++]);
}
}
//数值较大,未处理的数据直接放在临时数组末尾即可
while (i <= iMid)
{
vTemp.push_back(vSort[i++]);
}
while (j <= iEnd)
{
vTemp.push_back(vSort[j++]);
}
for (int k = iBeg; k <= iEnd; k++)
{
vSort[k] = vTemp[k-iBeg];//注意临时数组与源数组起始位置不同
}
return;
}
void MergeSortFunc(vector<int>& vSort)
{
int iSize = vSort.size();
MergeSort(vSort, 0, iSize - 1);
return;
}
6.快速排序
主要思想:以区段为单位,进行多次循环,任一次循环的目的是找到本次循环基准值的绝对位置,并以改位置将区段继续划分,递归下去;递归的出口是当划分后的子区段长度为1
注意事项:
(1)默认选择区段的首元素作为基准值
(2)取出基准值后,区段内的n-1个数值可以在n个位置上进行排序
(3)从右向左找小值,然后再从左向右找大值,交替覆盖
(4)当子区段的长度为1时,不需要再递归进行排序
代码实现:
void QuickSort(vector<int>& vSort,int iBeg,int iEnd)
{
int iKey = vSort[iBeg]; //取区段首元素为基准,将该值取出后,区段内其他元素占用该位置进行移动
int i = iBeg, j = iEnd;
while (i < j) //双指针相遇则退出循环
{
while (i<j&&vSort[j]>iKey)//从右向左,找到比Key值小的元素放在左指针当前位置上
{
j--;
}
vSort[i] = vSort[j];
while (i < j&&vSort[i] < iKey)//从左向右,找到比Key值大的元素放在右指针当前位置上
{
i++;
}
vSort[j] = vSort[i];
}
vSort[i] = iKey;//此时i==j,该位置为Key值在当前区段中的绝对位置,无需更改
if (i - iBeg > 1)//如果左侧元素大于2个
{
QuickSort(vSort, iBeg, i-1);
}
if (iEnd - i > 1)//如果右侧元素大于2个
{
QuickSort(vSort, i+1, iEnd);
}
return;
}
void QuickSortFunc(vector<int>& vSort)
{
int iSize = vSort.size();
QuickSort(vSort, 0, iSize - 1);
}
7.堆排序
主要思想:
(1)将数组排序为“大顶堆”,遵循从右至左,从下至上的策略,将每个人非叶子结点与其子节点进行对比,如果子节点大,则将该根节点与子节点交换
(2)排序为大顶堆后,此时堆顶元素为最大值,将堆顶元素与末尾元素交换
(3)每次交换后,重新将长度减小1的区间进行大顶堆排序,然后重复(2)步骤,直至结束
每一次大顶堆排序都获得该区间内的最大值,并将最大值放在区间末尾,将区间长度减去1,重复进行即可
注意事项:
(1)完全二叉树的首个飞叶子结点在数组中的下标为:vSort.size()/2-1
(2)进行大顶堆排序时,先找到两个子节点中较大的那一个,然后再与根节点进行判断,如果需要与根节点交换,记录该子节点的下标,下一次循环以该子节点为根节点(循环变量:i=i*2+1)
(3)进行大顶堆排序时,如果两个子节点的最大值都小于根节点,那么无需继续进行循环,直接break退出即可,因为采用从右至左,从下至上的策略,已经保证了当前的最大值
(4)堆顶元素与末尾元素交换完后后,应该注意“大顶堆”排序函数的参数3为长度值,而非下标值,不需要减去1
代码实现:
void HeapSort(vector<int>& vSort, int iBeg, int iLen)
{
int iTemp = vSort[iBeg];//取出该非叶子结点元素
//将其左右结点进行对比
for (int i = iBeg * 2 + 1; i < iLen; i=i*2+1)
{
//如果右子节点存在,并且小于左子节点,那么将当前指针移至右子节点
if (i<iLen - 1 && vSort[i]<vSort[i + 1])
{
i++;
}
if (vSort[i] > iTemp)//如果子节点的值大于父节点,将子节点赋值给父节点
{
swap(vSort[iBeg], vSort[i]);
//vSort[iBeg] = vSort[i];//与下列低7行的代码共同使用,可以替换swap函数
iBeg = i;//记录该结点位置,循环结束时将iTemp赋值给该结点
}
else//由于采用从下到上,从右至左的策略,只要两个子节点的最大值小于根节点,那么久无需向后继续判断
{
break;
}
//vSort[iBeg] = iTemp;//将当前值放置到其位置上即可(如果采用swap函数,只需要更新iBeg即可)
}
}
void HeapSortFunc(vector<int>& vSort)
{
//从第一个非叶子结点开始遍历,遵循从下至上,从右至左的策略,形成大顶堆
for (int i = vSort.size() / 2 - 1; i >= 0; i--)
{
HeapSort(vSort, i, vSort.size());
}
for (int i = vSort.size() - 1; i > 0; i--)
{
swap(vSort[0], vSort[i]); //将堆顶元素与末尾元素进行交换
HeapSort(vSort, 0, i); //将现有堆进行重新排序
}
return;
}
8.计数排序
主要思想:
创建单独的数组B,包括数组A中最小值到最大值的全部数值,数组B中的元素用于记录该位置对应数值出现的个数,最后循环数组B,覆盖数组A即可;
该方法主要用于较小区间内,多次数字重复出现情况下的排序
注意事项:
(1)创建计数数组后,应resize()大小为(iMax - iMin + 1);
(2)依照数组B的数据覆盖数组A时,如果数组B中某位置元素计数为0,则可以进行下一个位置的覆盖
代码实现:
void CountingSort(vector<int>& vSort)
{
int iSize = vSort.size();
int iMin = vSort[0], iMax = vSort[0];
for (int i = 0; i < iSize; i++)//获得数组内的最大值和最小值
{
iMin = min(vSort[i], iMin);
iMax = max(vSort[i], iMax);
}
vector<int> vCount;//创建计数数组
vCount.resize(iMax - iMin + 1);
for (int i = 0; i < iSize; i++)//记录iMin到iMax之间每个元素的出现次数
{
vCount[vSort[i] - iMin]++;
}
int iTemp = 0;
for (int i = 0; i < (int)vCount.size(); i++)
{
while (vCount[i] > 0)//只要该元素次数大于0,则存入数组中
{
vCount[i]--;
vSort[iTemp++] = iMin;
}
iMin++;//当前元素计数为0,则加1,进行下一个元素的判断
}
return;
}
9.桶排序
主要思想:
桶排序类似于计数排序,只不过计数排序中“桶”的大小为1,而桶排序中“桶”作为一个区间,将符合的数值全部添加进来,然后再对各个桶内的元素进行排序;
对各“桶”内元素进行排序时,可以递归调用桶排序,也可以采用其他排序方法
注意事项:
(1)采用map容器作为映射的接收容器,利用key值进行映射选择
(2)递归的出口是“桶”的大小为1时,也就是满足了计数排序的情况,在此处要特别注意,由于边界值的差最小为10,如果桶的个数选择过小,例如为5时,就会导致“桶”的容量始终无法达到1,出现死循环的情况
(3)创建全局变量vRes作为返回值,能否不创建这一问题后续考虑如何解决
代码实现:
vector<int> vRes;
void BucketSort(vector<int>& vSort, int iBucket)
{
int iSize = vSort.size();
map<int, vector<int>> mpBucket;//创建桶
int iMin = vSort[0], iMax = vSort[0];
for (int i = 0; i < iSize; i++)//获得数组内的最大值和最小值
{
iMin = min(vSort[i], iMin);
iMax = max(vSort[i], iMax);
}
int iLeft = (iMin / 10) * 10;//获得边界下限
int iRight = (iMax / 10 + 1) * 10;//获得边界上限
int iSection = max((iRight - iLeft) / iBucket,1);
for (int i = 0; i < iSize; i++)//将每一个数据映射到对应的桶中
{
//int iTemp = (vSort[i] - iMin) / iSection;
//if (mpBucket.find((vSort[i] - iMin) / iSection) == mpBucket.end())
//{
// vector<int> vTemp;
// //mpBucket.insert(pair<int, vector<int>>((vSort[i] - iMin) / iSection, vTemp));
// mpBucket[(vSort[i] - iMin) / iSection] = vTemp;
//}
mpBucket[(vSort[i] - iMin) / iSection].push_back(vSort[i]);
}
for (int i = 0; i < (int)mpBucket.size(); i++)//对每一个桶继续进行递归桶排序
{
if (iSection == 1)//注意递归出口值得选取一定要大于等于(iRight-iLeft)/iBucket的最小可能值
{
for (int j = 0; j < (int)mpBucket[i].size(); j++)
{
vRes.push_back(mpBucket[i][j]);
}
}
else
{
BucketSort(mpBucket[i], iBucket);
}
}
return;
}
void BucketSortFunc(vector<int>& vSort)
{
BucketSort(vSort, 10);//由于桶排序中设置的左右边界相差最小为10,而递归的出口选择分区大小为1,所以分区个数的选择至少为10,否则会出现死循环
vSort = vRes;
return;
}
10.基数排序
主要思想:
将一组数分别按照,个位,十位,百位,千位…数字的大小进行排序,每“位”对应一层,由小至大,最高层的大小取决于最大值的位数,由于数字为0-9,所以每层需要10个区间,最后一层排序完成后,按照0-9的区间顺序输出数据就是最后从小到大的排列
注意事项:
(1)数据结构为vector<vector<queue>>的三维结构,第一维是最大值的位数;第二维是0-9,对应各位的数字;第三维是队列,用来存储符合该队列要求的数值(为什么采用队列进行先进先出后续研究一下)
(2)第一层排列(按照个位进行排列)时,从原数组进行遍历,存储至自定义的数据结构中
(3)从第二层开始,依照上一层的排序来实现当前层的排序,除取得当前层对应数位的数值时需要先除去(/)在取余(%)之外,其他与第二点相同
代码实现:
void RadixSort(vector<int>& vSort)
{
int iSize = vSort.size();
int iMax = 0;//获取数组中最大值
for (int i = 0; i < iSize; i++)
{
iMax = max(iMax, vSort[i]);
}
vector<RadixSTL> vRadix;//定义各排列方式的容器(vector<queue<int>>)
int iSizeRadix = 0;
while (iMax)
{
iSizeRadix++;
iMax /= 10;
}
vRadix.resize(iSizeRadix);//根据最大值位数设置容器大小
for (int i = 0; i < iSizeRadix; i++)
{
vRadix[i].resize(10);//将每个数组内队列个数设置为10个,对应0-9
}
//开始进行排列
for (int i = 0; i < iSize; i++)
{
vRadix[0][vSort[i] % 10].push(vSort[i]);//根据余数将数值存储到对应的队列中,得到由个位排序的队列数组
}
//此处开始循环,根据上一层的10个队列元素,获得当前层10个队列元素的排列
int iTimes = 1;
while (iTimes < iSizeRadix)
{
for (int i = 0; i < 10; i++)
{
while (!vRadix[iTimes - 1][i].empty())
{
int iNum = vRadix[iTimes - 1][i].front();//获取每个队列的队首元素;
int iTemp = iNum / (int)pow((double)10, (double)(iTimes));//获取该元素
vRadix[iTimes][iTemp % 10].push(iNum);//根据当前位的数值,存储到该层对应的队列中
vRadix[iTimes - 1][i].pop();//将该元素从队首取出
}
}
iTimes++;
}
//将最顶层队列数组的全部数值按照顺序拷贝到原数组中
int iIndex = 0;
for (int i = 0; i < 10; i++)
{
while (!vRadix[iSizeRadix - 1][i].empty())
{
vSort[iIndex++] = vRadix[iSizeRadix - 1][i].front();
vRadix[iSizeRadix - 1][i].pop();
}
}
return;
}
至此,十大经典排序算法均复现完成,其中也遇到一些问题调试很久,如堆排,快排和归并排序都话费了一定的时间才完成,排序算法虽然基本,但也经常会在手撕算法中被考察,总结并理解十大算法的时间、空间复杂度并加以理解同样重要。
经过复现的过程,个人认为掌握各排序算法的排序精髓最为重要,清楚地认知到每次循环以获得何种效果为目的,相信在面试中被考量到该算法的编写时,仔细考虑到边界问题,便可顺利完成调试。