排序算法分类
排序算法的性能
稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
时间复杂度:对排序数据的总的操作次数。反映当n变化时,操作次数呈现什么规律。
空间复杂度:是指算法在计算机内执行时所需存储空间的度量,它也是数据规模n的函数。
(1)冒泡排序
冒泡排序通过循环比较将最大的元素放到最后,因此形象的称之为“冒泡”过程。从左向右依次两两比较大小,若左边的值大于右边的值,则两两交换,经过一次排序后,最后面的值则为整个数组中最大的值。重复经过n轮遍历,便可以完成整个数组的排序。
算法描述:
①从第一个元素开始,比较第i个和第i+1个元素
②若i的值大于i+1的值,则交换连个元素,直到i+1为数组的最后一个元素
③再次从第一个元素开始,重复①②工作
算法优化:
经过第一轮比较后,最后面的元素已经排好序,因此第二轮比较时,i+1为数组的倒数第二个元素便可以退出循环了,每经过一轮排序,均可以较少一个比较
可以设置一个标志位,若经过一轮比较,未发生过交换,则说明现在数组已经排好顺序了,可以直接退出
算法动图:
void BubbleSort(vector<int> &input_vec)
{
if (input_vec.empty() || input_vec.size() == 1)
return;
bool swap_flag = true;
for (int i = 0; i < input_vec.size() - 1 && swap_flag; i++)
{
swap_flag = false;
for (int j = 0; j < input_vec.size() - i - 1; j++)
{
if (input_vec[j] > input_vec[j + 1])
{
swap_flag = true;
int temp = input_vec[j];
input_vec[j] = input_vec[j + 1];
input_vec[j + 1] = temp;
}
}
}
}
(2) 选择排序
选择排序通过遍历查找出最小元素位置,并进行排序。首先在未排序序列中找到最小元素,将其和序列中的第一个元素交换;然后,再从剩余未排序元素中继续寻找最小元素并进行排序。以此类推,直到所有元素均排序完毕。
算法描述
①遍历整个数组,找到最小位置元素的位置
②将其和序列的第一个元素进行交换,则第一个位置元素已排好序,对剩余部分继续排序
③对剩余未排序序列重复①②操作,直至整个序列都排好序
算法动图
void SelectSort(vector<int> &input_vec)
{
for (int i = 0; i < input_vec.size(); i++)
{
int min = input_vec[i];
for (int j = i; j < input_vec.size(); j++)
{
if (input_vec[j] < min)
{
min = input_vec[j];
int temp = input_vec[i];
input_vec[i] = input_vec[j];
input_vec[j] = temp;
}
}
}
}
(3)简单插入排序
插入排序是基于已排序序列,将一个元素插入到有序队列的合适位置之中。相较于冒泡排序和选择排序,三种排序的复杂度都较高,达到O(n^2),冒泡和选择排序也不可能使用到应用实践中去,都只能停留在理论研究上,而插入排序还是以其原地排序和稳定性能够在某些特定的场景中使用。在快速排序中,为减少递归的深度,可以采用插入排序做优化。
算法描述
①首先对前两个元素进行比较并排序
②将第三个元素放入到前两个有序数组的合适位置,完成后,前三个元素为有序数组
③再对下一个元素重复①②步骤,直至所有元素排序完成
算法动图
算法实现
void InsertSort(vector<int> &input_vec)
{
for (int i = 1; i < input_vec.size(); i++)
{
int pre = i - 1;
int current = input_vec[i];
for (; pre >= 0 && input_vec[pre] > current; pre--)
input_vec[pre + 1] = input_vec[pre];
input_vec[pre + 1] = current;
}
}
对第i个元素来说,前面i-1个元素是拍好顺序的,因此,如果前面有序数组中第k(0<=k<=i-1)个元素大于第i个元素,则k~i-1的元素都会大于它,因此,从第i-1位置的元素开始,依次往后挪一位,最后在空的位置将原始的第i个元素的值填入,更好的利用了前部数组的有序条件
(4)希尔排序
1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。对于插入排序来说,一个“基本有序”的数组可以更少的减小循环次数。希尔排序先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录“基本有序”时,再对全体记录进行依次直接插入排序。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。
算法优点
简单插入排序很循规蹈矩,不管数组分布是怎么样的,依然一步一步的对元素进行比较,移动,插入,比如[5,4,3,2,1,0]这种倒序序列,数组末端的0要回到首位置很是费劲,比较和移动元素均需n-1次。而希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。希尔排序通过这种策略使得整个数组在初始阶段达到从宏观上看基本有序,小的基本在前,大的基本在后。然后缩小增量,到增量为1时,其实多数情况下只需微调即可,不会涉及过多的数据移动。
算法描述
在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。希尔排序的增量序列的选择与证明是个数学难题,我们选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量,但其实这个增量序列不是最优的。此处我们做示例使用希尔增量。
算法实现
void ShellSort(vector<int> &input_vec)
{
int gap = (input_vec.size()) / 2;
while (gap >= 1)
{
for (int i = gap; i < input_vec.size(); i++)
{
int pre = i - gap;
int current = input_vec[i];
for (; pre >= 0 && input_vec[pre] > current; pre -= gap)
input_vec[pre + gap] = input_vec[pre];
input_vec[pre + gap] = current;
}
gap /= 2;
}
}
(5)归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
算法描述(迭代法)
①申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
②设定两个指针,最初位置分别为两个已经排序序列的起始位置
③比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
④重复步骤③直到某一指针到达序列尾
⑤将另一序列剩下的所有元素直接复制到合并序列尾
算法描述(递归法)
①将序列每相邻两个数字进行归并操作,形成n/2个序列,排序后每个序列包含两个元素
②将上述序列再次归并,形成n/4个序列,每个序列包含四个元素
③重复步骤②,直到所有元素排序完毕
从上至下为递归,从下至上为迭代
算法实现(递归法)
void MergeSort(vector<int> &input_vec, int left, int right) {
if (left >= right) return;
int mid = (left + right) >> 1;
MergeSort(input_vec, left, mid);
MergeSort(input_vec, mid + 1, right);
int start1 = left, end1 = mid, start2 = mid + 1, end2 = right;
vector<int> temp;
while (start1 <= end1 && start2 <= end2) {
temp.push_back(input_vec[start1] < input_vec[start2] ? input_vec[start1++] : input_vec[start2++]);
}
while (start1 <= end1) temp.push_back(input_vec[start1++]);
while (start2 <= end2) temp.push_back(input_vec[start2++]);
for (int k = 0; k < temp.size(); k++) {
input_vec[left + k] = temp[k];
}
}
算法实现(迭代法)
void MergeSort(vector<int> &input_vec)
{
for (int step = 1; step <= input_vec.size(); step = step * 2)
{
int len = input_vec.size() % (2 * step) == 0 ? input_vec.size()
: input_vec.size() + 2 * step;
for (int j = 0; j < len; j += 2 * step)
{
vector<int> temp;
temp.clear();
int start1 = j, start2 = j + step;
while (start1 < j + step && start1 < input_vec.size() &&
start2 < j + step * 2 && start2 < input_vec.size())
temp.push_back(input_vec[start1] < input_vec[start2] ? input_vec[start1++]
: input_vec[start2++]);
while (start1 < j + step && start1 < input_vec.size())
temp.push_back(input_vec[start1++]);
while (start2 < j + step * 2 && start2 < input_vec.size())
temp.push_back(input_vec[start2++]);
for (int k = 0; k < temp.size(); k++)
input_vec[j + k] = temp[k];
}
}
}
(6)快速排序
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。快速排序使用分治法来把一个串分为两个子串。
算法描述
①从数列中挑出一个元素,称为 “基准”(pivot);
②重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
③递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
算法动图
void QuickSort(vector<int>&vec, int low, int high)
{
if (low < high) {
int i = low, j = high;
int temp = vec[low];
while (i < j) {
while (i < j && vec[j] >= temp) j--; // 从右向左找第一个小于k的数
if (i < j) vec[i++] = vec[j];
while (i < j && vec[i] <= temp) i++; //从左到右寻找第一个大于k的数
if (i < j) vec[j--] = vec[i];
}
vec[i] = temp;
// 递归调用
QuickSort(vec, low, i - 1); // 排序k左边
QuickSort(vec, i + 1, high); // 排序k右边
}
}
(7)堆排序
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序。
堆结构
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
算法描述
①将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
②将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
③由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
算法动图
算法实现:
void heap_swap(vector<int> &input_vec, int pos1, int pos2)
{
if (input_vec.empty() || pos1 >= input_vec.size() || pos2 >= input_vec.size())
return;
int temp = input_vec[pos1];
input_vec[pos1] = input_vec[pos2];
input_vec[pos2] = temp;
}
void heap_copare(vector<int> &input_vec, int parent, int limit)
{
if (input_vec.empty())
return;
int ch1 = (parent << 1) + 1, ch2 = (parent << 1) + 2, flag = parent;
if (ch1 < limit && input_vec[ch1] > input_vec[flag])
flag = ch1;
if (ch2 < limit && input_vec[ch2] > input_vec[flag])
flag = ch2;
if (flag != parent)
{
heap_swap(input_vec, parent, flag);
heap_copare(input_vec, flag, limit);
}
}
void HeapSort(vector<int> &input_vec)
{
if (input_vec.empty() || input_vec.size() == 1)
return;
//构建最大堆
int last = input_vec.size() - 1, parent = (last - 1) / 2;
for (int idx = parent; idx >= 0; idx--)
heap_copare(input_vec, idx, input_vec.size());
//交换并修复堆
for (int idx = last; idx >= 0; idx--)
{
heap_swap(input_vec, 0, idx);
heap_copare(input_vec, 0, idx);
}
}
(8)计数排序
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。
算法描述
①找出待排序的数组中最大和最小的元素;
②统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
③对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
④反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。
算法实现
void CountingSort(vector<int> &input_vec, int min, int max)
{
if (input_vec.empty() || min > max)
return;
vector<int> temp;
temp.resize(max - min + 1);
for (int idx = 0; idx < input_vec.size(); idx++)
temp[input_vec[idx] - min]++;
int k = 0;
for (int idx = 0; idx < temp.size(); idx++)
for (int j = 0; j < temp[idx]; j++)
input_vec[k++] = idx + min;
}
(9)桶排序
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。
算法描述
①设置一个定量的数组当作空桶;
②遍历输入数据,并且把数据一个一个放到对应的桶里去;
③对每个不是空的桶进行排序;
④从不是空的桶里把排好序的数据拼接起来。
void BucketSort(vector<int> &input_vec, int min, int max, int bucket_size)
{
if (input_vec.empty() || min > max || bucket_size <= 0)
return;
vector<vector<int>> bucket;
bucket.resize((max - min + 1) / bucket_size + 1);
for (int idx = 0; idx < input_vec.size(); idx++)
bucket[(input_vec[idx] - min) / bucket_size].push_back(input_vec[idx]);
int k = 0;
for (int idx = 0; idx < bucket.size(); idx++)
{
InsertSort(bucket[idx]);
for (int j = 0; j < bucket[idx].size(); j++)
input_vec[k++] = bucket[idx][j];
}
}
(10) 基数排序
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位
。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
算法描述
①取得数组中的最大数,并取得位数;
②arr为原始数组,从最低位开始取每个位组成radix数组;
③对radix进行计数排序(利用计数排序适用于小范围数的特点;
算法动图
void CArithmetic::RadixSort(vector<int> &input_vec)
{
if (input_vec.empty())
return;
vector<vector<int>> bucket;
int flag = true;
for (int times = 1, dev = 1, count = 0; flag; times *= 10, dev *= 10)
{
bucket.resize(10);
bucket.clear();
flag = false;
for (int idx = 0; idx < input_vec.size(); idx++)
{
int p = (input_vec[idx] % (10 * times)) / dev;
bucket[p].push_back(input_vec[idx]);
if (p)
count++;
}
cout << count << endl;
if (count != 0)
{
flag = true;
int k = 0;
for (int idx = 0; idx < bucket.size(); idx++)
{
for (int j = 0; j < bucket[idx].size(); j++)
{
cout << " " << bucket[idx][j];
input_vec[k++] = bucket[idx][j];
}
}
cout << endl;
}
}
}