通过排序表中两个元素的比较,若与排序要求相逆(不符合升序或降序),则将两者交换。
基本思想:任取待排序序列中的某个元素作为标准(也称为支点、界点,一般取第一个元素),通过一次划分,将待排元素分为左右两个子序列,左子序列元素的排序码均小于基准元素的排序码,右子序列的排序码则大于或等于基准元素的排序码,即找到该元素在数组中顺序排序的位置。然后分别对两个子序列继续进行划分,直至每一个序列只有一个元素为止。最后得到的序列便是有序序列。
int Partition(int* arr, int left, int right)
{
int key = arr[left];
while(left < right)
{
while(left < right && arr[right] >= key) right--;
arr[left] = arr[right];
while(left < right && arr[left] <= key) left++;
arr[right] = arr[left];
}
arr[left] = key;
return left;
}
void Quick(int* arr, int start, int end)
{
if(start < end)
{
int index = Partition(arr, start, end);
Quick(arr, start, index - 1);
Quick(arr, index + 1, end);
}
}
void QuickSort(int* arr, int len)
{
Quick(arr, 0, len - 1);
}
//非递归快速排序
void QuickSort(int* arr, int len)
{
std::stack s;
int left = 0;
int right = len - 1;
int index = Partition(arr, left, right);
//第一次划分,得到两个待排序列,分别记录(入栈)
//小于基准元素左序列
if(index - 1 > left)
{
s.push(left); //左序列的开始
s.push(index - 1); //左序列的结束
}
//小于基准元素右序列
if(index + 1 < right)
{
s.push(index + 1);//右序列的开始
s.push(right);//右序列的结束
}
//得到新的序列(出栈),对该序列再进行划分,栈为空则无法划分(排序完成)
while(!s.empty())
{
right = s.top();
s.pop();
left = s.top();
s.pop();
index = Partition(arr, left, right);
if(index - 1 > left)
{
s.push(left);
s.push(index - 1);
}
if(index + 1 < right)
{
s.push(index + 1);
s.push(right);
}
}
}
效率分析:如果每次划分对一个对象定位后,该对象的左子序列与右子序列的长度相同, 则下 一步将是对两个长度减半的子序列进行排序, 这是最理想的情况。快速排序的最好时间复杂度为O(nlog2n)。在最坏的情况, 即待排序对象序列已经按其排序码从小到大排好序的情况下,快速排序的最坏时间复杂度为O(n2)。
快速排序最好的空间复杂度为O(log2n),最坏的空间复杂度为O(n)(即快速排序所需用的辅助空间)。
快速排序是一种不稳定的排序方法。
基于快速排序的思想,每一次划分,两个新序列长度越接近,排序的总效率越高(数组中的数越随机越好)。
1、三数取中法(将开始、中间、结尾坐标互换),对基本有序的数组进行优化。
//三数取中,使left下标元素大小处于中间
void GetMidNum(int*arr, int left, int mid, int right)
{
if(arr[left] > arr[right])
Swap(arr, left, right);
if(arr[mid] > arr[left])
Swap(arr, mid, left);
if(arr[mid] > arr[right])
Swap(arr, mid, right);
}
2、随机选取表准基数,对于基本有序数组进行优化。
//随机选取基准
srand((unsigned int)time(NULL));
Swap(arr, start, rand()%(end - start +1) + start);
3、对于小序列采用插入排序,当待排序序列的长度分割到一定大小后,继续分割的效率比插入排序要差。
//对于小序列采用插入排序
if(end - start < 10)
{
InsertSort(arr, end - start +1);
return ;
}
else
{
//快速排序
}
4、处理重复元素:划分过程中将与表准基数相同的数放在两端并记录;换分完成后,再将相同元素交换到表准基数两旁。
int Partition(int* arr, int low, int high, int& repeat)
{
int left = low;
int right = high;
int key = arr[left];
int leftlen = 0;
int rightlen = 0;
while(left < right)
{
while(left < right && arr[right] >= key)
{
//处理相等元素
if(arr[right] == key)
{
Swap(arr, right, high-rightlen);//把右边与key元素相等的聚集的右端
++rightlen;
}
right--;
}
arr[left] = arr[right];
while(left < right && arr[left] <= key)
{
//处理相等元素
if(arr[left] == key)
{
Swap(arr, left, low+left);//把左边与key元素相等的聚集数组的左端
++leftlen;
}
left++;
}
arr[right] = arr[left];
}
arr[left] = key;
repeat = 0; //记录重复出现的次数
repeat = leftlen + rightlen;
//相同元素交换到表准基数两旁
for(int i = 0; i < leftlen; i++)
{
Swap(arr, low + i, left - i -1);
}
for(int i = 0; i < rightlen; i++)
{
Swap(arr, high - i, left +i +1);
}
return left - leftlen; //返回重复数字最左端下标
}
通过对待排序列从前向后,依次比较相邻元素的大小,如果逆序则交换,使较大的元素逐渐后移。
效率分析:从冒泡排序的算法可以看出,若待排序的元素为正序,则只需进行一趟排序,比较次数为(n-1)次,移动元素次数为0;若待排序的元素为逆序,则需进行n-1趟排序,比较次数为(n2-n)/2,移动次数为3(n2-n )/2,因此冒泡排序算法的时间复杂度为O(n2)。顺序情况下为O(n),由于其中的元素移动较多,所以属于内排序中速度较慢的一种。
因为冒泡排序算法只进行元素间的顺序移动,所以是一个稳定的算法。
void BubbleSort(int* arr, int len)
{
int tmp = 0;
bool flag = false;
for(int i = 0; i < len-1; ++i)
{
for(int j = 0; j < len -1 -i; ++j)
{
flag = false;
//将较大元素后移
if(arr[j+1] < arr[j])
{
flag = true;
tmp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = tmp;
}
}
//没有交换,结束
if(!flag)
{
break;
}
}
}