这里把排序算法分为易、中、难三类:
易篇
1. 冒泡排序
上图是一个逆向升序排列
理解: 有n个数据,划分为有序区和无序区,比较两个数据大小,并根据排序方向交换称为一个冒泡过程,从起始位置完成多次冒泡获得一个最值称为一趟冒泡,每完成一趟冒泡有序区多一,完成 i 趟意味着有序区(不需要再次冒泡)数据量为 i 无序区数量为 n-i ,完成所有数据排序需要 n-1 趟,每趟需要冒泡 n - i - 1 次。且如果在某一趟没有发生交换则认为排序完成。时间复杂度O(n2)。
C++代码如下:
//升序
void GululuSort(vector<int>& nums)
{
int n = nums.size();
for (int i = 0; i < (n - 1); i++) { //一趟冒泡
bool isExchange = false;
for (int j = 0; j < (n - i - 1); j++)//一次冒泡
{
if (nums[j] > nums[j + 1])
{
swap(nums[j], nums[j + 1]);
isExchange = true;
}
}
cout << "第" << i + 1 << "次:";
for (int i : nums)
{
cout << i << " ";
}
cout <<"isExchange="<< isExchange << endl;
if (!isExchange)
return;
}
}
执行结果:
优点: 对于只有少数混乱的数据,冒泡排序冒泡趟数少。
缺点: 每次冒泡都要进行数据交换,花费之间长。
2. 选择排序
理解: 其实选择排序和冒泡排序很像,区别就在于对数据的交换方式上,我们也将数据划分为有序区和无序区,通过记录位置的方式将无序区的最值逐个放入有序区。n个数据完成排序需要选择n-1趟,每趟需要比较n-1-i次。时间复杂度O(n2)。
C++代码如下:
void SelectSort(vector<int>& nums)
{
int n = nums.size();
for (int i = 0; i < n-1; i++) //一趟选择
{
int max_loc = n - i - 1; //假设无序区最后一个值为最大值
for (int j = 0; j < n - i - 1; j++)
{
if (nums[j] > nums[max_loc])
{
max_loc = j; //比较记录一次
}
}
swap(nums[max_loc], nums[n - i - 1]);//交换数据
cout << "第" << i+1 << "趟" << " ";
for (auto i : nums)
{
cout << i << " ";
}
cout << endl;
}
}
执行结果:
优点: 相对冒泡排序仅在每趟的最后交换数据,从这个角度上看选择排序是快于冒泡排序的。
缺点: 对于顺序已经排好的或者大多数数据位置都是有序的,依然需要选择同样的趟数。
3. 插入排序
理解: 把插入过程比作摸牌,假设第一个数据为手里面已有的牌,每次取一张牌和手里的牌对比,大于手里当前牌则将牌插入,否则手里将当前牌向后移动一个位置,然后与手里下一张比较,如此一直遍历完手里的牌将其插入。复杂度O(n2)。
void InsertSort(vector<int>& nums)
{
int n = nums.size();
for (int i = 1; i < n; i++) //一次摸牌
{
int tmp = nums[i]; //摸到的牌
int j = i - 1;
for (; j >= 0; j--) //遍历手里牌
{
if (nums[j] < tmp) //找到摸到牌在手里位置
{
break;
}
else
{
nums[j + 1] = nums[j]; //当前手里牌向后移动
}
}
nums[j + 1] = tmp; //插入摸到牌
cout << "第" << i << "张" << " ";
for (auto i : nums)
{
cout << i << " ";
}
cout << endl;
}
}
优点: 如果输入数据是一个链表则不需要移动,仅插入即可。
缺点: 如果输入数据是一个数组需要移动数据。
中篇
难篇
总结
- 易篇中三种排序算法时间复杂度都为O(n2),针对不同的数据场景可对比选择。