基本思想
两两比较待排序元素的大小,发现两个元素的次序相反时即进行交换,直到序列全部有序为止。
冒泡排序
1.排序思路
对无序区每两个相邻的元素进行比较,小元素换至大元素之后(之前),经过一趟排序后,最小的元素到达最后端(最前端)。接着,再在剩下的元素中重复上述过程。这样,通过无序区中相邻元素的比较和交换,可以使得最小的元素像如气泡一般逐渐往上“漂浮”。
2.排序算法
void BubbleSort(int *arr, int size)
{
if (arr == NULL)
return;
int i, j;
for (i = 0; i < size-1; i++)
{
//正序
/*for (j = size - 1; j > i; j--)
{
if (arr[j] < arr[j - 1])
{
int tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
}
}*/
//逆序
for (j = 0; j < size - 1 - i; j++)
{
if (arr[j] < arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
3.算法分析
当初始数据为正序时,一趟扫描即可完成排序,此时最好时间复杂度为O(N);当初始数据为逆序时,则需要进行n-1趟排序,且每趟排序要进行n-i-1次比较,每次比较都会交换元素,此时最坏时间复杂度为O(N^2)。
故,冒泡排序的时间复杂度为O(N^2),空间复杂度为O(1)。虽然冒泡排序不一定要进行n-1趟,但它的元素移动次数较多,所以平均时间性能比直接插入排序要差。
冒泡排序是一种稳定的排序算法。
4.算法优化
当某趟排序之后,序列已经完全有序时,大可不必继续排序,于是我们设置一个标志为flag,在每趟排序的开始重置flag,若该趟排序结束时,flag没有变化,则证明序列已经有序,即可break退出循环。
void BubbleSort1(int *arr, int size)
{
if (arr == NULL)
return;
int i, j, flag;
for (i = 0; i < size - 1; i++)
{
flag = 1;
for (j = size - 1; j>i; j--)
{
if (arr[j] < arr[j - 1])
{
int tmp = arr[j];
arr[j] = arr[j - 1];
arr[j - 1] = tmp;
flag = 0;
}
}
if (flag == 1) //说明此趟排序没有进入内循环,已经有序
{
break;
}
}
}
快速排序
1.排序思路
在待排序的n个元素中任取一个元素作为基准,数据序列被此元素划分成两部分,比该元素小的所有元素放在前边部分,比该元素大的所有元素放在后边部分,并把该元素排在这两部分的中间。这个过程称为一趟快速排序,之后对划分出来的两部分分别重复上述过程,直至每部分内只有一个或没有元素为止。
2.排序算法
//1.左右指针法
int _PartSort1(int *arr, int begin, int end)
{
int key = arr[end];
int left = begin;
int right = end;
while (left < right)
{
while (left < right && arr[left] <= key)
{
left++;
}
while (left < right&&arr[right] >= key)
{
right--;
}
swap(arr[left], arr[right]);
}
swap(arr[left], arr[end]);
return left;
}
//2.挖坑法
int _PartSort2(int *arr, int begin, int end)
{
int key = arr[end];
while (begin < end)
{
while (begin < end && arr[begin] <= key)
{
begin++;
}
arr[end] = arr[begin]; //end当坑
while (begin < end && arr[end] >= key)
{
end--;
}
arr[begin] = arr[end]; //begin当坑
}
arr[begin] = key;
return begin;
}
//2.前后指针法
int _PartSort3(int *arr, int begin, int end)
{
int prev = begin;
int cur = begin+1;
while (cur <= end)
{
if (arr[cur] <= arr[begin])
{
prev++;
if (prev != cur)
{
swap(arr[prev], arr[cur]);
}
cur++;
}
else
{
cur++;
}
}
swap(arr[prev], arr[begin]);
return prev;
}
void Quick(int *arr, int begin, int end)
{
if (arr == NULL)
return;
if (begin < end)
{
//int div = _PartSort1(arr, begin, end);
//int div = _PartSort2(arr, begin, end);
int div = _PartSort3(arr, begin, end);
Quick(arr, begin, div - 1);
Quick(arr, div + 1, end);
}
}
3.算法分析
快速排序的时间主要耗费在划分操作上,当初始数据递增有序或递减有序,且每次的划分基准都取最小值或最大值时,则快速排序所需的比较次数最多,最坏时间复杂度为O(N^2)。在最好情况下,每次划分基准值都取当前无序区的中值时,划分的左右两个子序列的长度大致相等,此时的时间复杂度可用递归树来分析,递归树的高度为lgN,而递归树每一层所需要的比较次数不超过N次,则整个排序过程的总比较次数为N*lgN,故最好时间复杂度为O(N*lgN)。
注意,快速排序的时间复杂度不看最坏情况!故快速排序的时间复杂度为O(N*lgN)。递归平均所需的栈空间为lgN,故快速排序的空间复杂度为O(lgN)。快速排序是一种不稳定的排序算法。
4.算法优化
三数取中法:为了防止所选划分基准值为序列最大值或最小值,用三数取中法来确定基准值,提高算法效率。
int GetMidIndex(int *a, int begin, int end)
{
int mid = begin + ((end - begin) >> 1);
if (a[begin] < a[mid])
{
if (a[end] > a[mid])
{
return mid;
}
else if (a[end] < a[begin])
{
return begin;
}
else
{
return end;
}
}
else //a[begin] >= a[mid]
{
if (a[end] < a[mid])
{
return mid;
}
else if (a[end] > a[begin])
{
return begin;
}
else
{
return end;
}
}
}
小区间划分:优化递归的层数,对接近有序的情况不再递归,使用插入排序。
void QuickSort(int *arr, int begin, int end)
{
if (arr == NULL)
return;
if (begin < end)
{
if (end - begin > 10)
{
int div = _PartSort1(arr, begin, end);
QuickSort(arr, begin, div - 1);
QuickSort(arr, div + 1, end);
}
else
{
InsertSort(arr + begin, end - begin + 1);
}
}
}
快速排序的非递归实现
1.排序思路
把递归问题转换为非递归问题,可以借助栈来实现,我们可以把每个子区间的begin和end保存在栈中,然后成对取出,对子区间排序,接着又把划分出来的子区间的begin和end保存在栈中,重复上述过程,直至子区间的元素只剩下一个或为空。
2.排序算法
oid QuickSortNonR(int *arr, int begin, int end)
{
if (arr == NULL)
return;
int left, right;
stack<int> s;
s.push(begin);
s.push(end);
while (!s.empty())
{
right = s.top();
s.pop();
left = s.top();
s.pop();
if (left < right)
{
int div = _Part1(arr, left, right);
//前部分子区间
s.push(left);
s.push(div - 1);
//后部分子区间
s.push(div + 1);
s.push(right);
}
}
}