排序算法的稳定性:假定在待排序的记录序列中存在多个具有相同关键码的记录,若经过排序,这些记录的相对次序保持不变,则称这种排序算法稳定,否则称为不稳定。
排序算法的稳定性只是算法的一种属性,且由具体算法决定
根据排序过程中所有记录是否全部放在内存中,排序方法分为:
(1)内排序:在排序的整个过程中,待排序的所有记录全部放在内存中
(2)外排序:待排序的记录个数较多,整个排序过程需要在内外存之间多次交换数据才能得到排序的结果
根据排序方法是否建立在关键码比较的基础上,排序方法分为:
(1)基于比较:主要通过关键码之间的比较和记录的移动实现
(2)不基于比较:根据待排序数据的特点所采取的其他方法
稳定排序、不稳定排序
如何衡量排序算法的性能呢?
class Sort
{
public:
Sort(int r[ ], int n);
~Sort( );
void InsertSort( );
void ShellSort( );
void BubbleSort( );
void QuickSort(int first, int last);
void SelectSort( );
void HeapSort( );
void MergeSort1(int first, int last);
void MergeSort2( );
void Print( );
private:
int Partition(int first, int last);
void Sift(int k, int last);
void Merge(int first1, int last1, int last2);
void MergePass(int h);
int *data;
int length;
Sort :: Sort(int r[ ], int n)
{
data = new int[n];
for (int i = 0; i < n; i++)
data[i] = r[i];
length = n;
}
Sort :: ~Sort( )
{
delete[ ] data;
}
void Sort :: Print( )
{
for (int i = 0; i < length; i++)
{
cout << data[i] << "\t";
}
cout << endl;
}
直接插入排序的基本思想:依次将待排序序列中的每一个记录插入到已排好序的序列中,直到全部记录都排好序。
在插入第 i(i>1)个记录时,前面的 i-1个记录已经排好序
1找到插入位置
2将插入位置及其后的元素后移一位
3插入
for (i = 1; i < length; i++)
{
插入第 i 个记录,即第 i 趟直接插入排序;
}
void Sort :: InsertSort( )
{
int i, j, temp;
for (i = 1; i < length; i++)
{
temp = data[i];
data[0] = temp
j = i - 1;
while (j >= 0 && temp < data[j])/或(因为有哨兵)/(temp < data[j])
{
data[j + 1] = data[j];
j--;
}
data[j + 1] = temp;
}
}
希尔排序的基本思想:将待排序序列分割成若干个子序列,在子序列内分别进行直接插入排序,待序列基本有序时,对全体记录进行直接插入排序。
基本有序:接近正序,例如{1, 2, 8, 4, 5, 6, 7, 3, 9}。
{5, 6, 7, 8, 9, 1, 2, 3, 4}是基本有序吗?
**局部有序(部分有序)**不能提高直接插入排序算法的时间性能。
如何分割待排序序列,才能使整个序列逐步向基本有序发展?
不能是逐段分割,而是将相距某个增量的记录组成一个子序列
如何分割待排序序列,才能使整个序列逐步向基本有序发展?
(1)希尔排序的时间性能取决于增量序列,复杂的数学问题。
(2)希尔排序是平均时间性能好于O(n2)的第一批算法之一(1959年)
(3)希尔最早提出的方法是 ,且增量序列互质。
显然最后一个增量必须等于 1——缩小增量排序
一般会给定增量序列,主要是学习改进算法的思想
void Sort :: ShellSort( )
{
int d, i, j, temp;
for (d = length/2; d >= 1; d = d/2) //增量为d进行直接插入排序
{
for (i = d; i < length; i++) //进行一趟希尔排序
{
temp = data[i]; //暂存待插入记录
for (j = i - d; j >= 0 && temp < data[j]; j = j - d)//从后往前,如果大于后一个,就往后移d位。
data[j + d] = data[j]; //记录后移d个位置
data[j + d] = temp;
}
}
}
时间性能:O(n2) ~ O(nlog2n)
(1)希尔排序算法的时间性能是所取增量的函数;
(2)研究表明,希尔排序的时间性能在O(n2)和O(nlog2n)之间;
(3)如果选定合适的增量序列,希尔排序的时间性能可以达到O(n1.3) 。
空间性能:O(1)——暂存单元
稳定性:不稳定
bound = exchange; exchange = 0;
for (j = 0; j < bound; j++)
if (data[j] > data[j+1]){
temp = r[j]; r[j] = r[j+1]; r[j+1] = temp;
exchange = j;
}
while (exchange != 0)
{
执行一趟起泡排序;
}
void Sort :: BubbleSort( )
{
int j, exchange, bound, temp;
exchange = length - 1; //第一趟起泡排序的区间是[0~length-1]
while (exchange != 0)
{
bound = exchange; exchange = 0;
for (j = 0; j < bound; j++) //一趟起泡排序的区间是[0~bound]
if (data[j] > data[j+1]) { #比较语句?执行次数?
temp = data[j]; data[j] = data[j+1]; data[j+1] = temp; #移动语句?执行次数?
exchange = j; //记载每一趟的最后一次交换的位置
}
}
}
改进冒泡排序中一次交换只能消除一个逆序的缺点,即实现一次交换消除多个逆序。
问题:记录的比较在相邻单元中进行、每次交换只能右移一个单元、总的比较次数和移动次数较多
改进:较大记录从前面直接移到后面,较小记录从后面直接移到前面?
快速排序的基本思想:
选一个轴值,将待排序记录划分成两部分,
左侧记录均小于或等于轴值,
右侧记录均大于或等于轴值,
即一次划分:以轴值为基准将无序序列划分为两部分,
之后分别对分割所得两个子序列“递归”进行快速排序,直到整个序列有序。
显然,快速排序是一个递归的过程
void Sort :: QuickSort (int first, int last )
{
int pivot = Partition (first, last);
QuickSort (first, pivot-1);
QuickSort (pivot+1, last );
}
if (first == last) return;
将第一个记录元素放在r[0]上;
low、high指向第一个和最后一个元素
先比较high->98和枢轴值48,大于则跳过,小于则赋值给low
再比较low和枢轴值48,小于则跳过,大于则赋值给high
int QKpass (RecordType r[]int low,int high)
{
r[0] = r[low];
while (low<high)
{
while(low<high&&r[high].key>=r[0].key)
--high;
r[low] = r[high];
while (low<high &r[low].key<=r[0].key)
++low;
r[high] = r[low];
}
r[low] = r[0]; return low;
}
void QKSort(RecordType r[],int low,int high)
{
r[0]=r[low];
if(low<high)
{
pos=QKpass(r,low,high);
QKSort(r,low,pos-1);
QkSort(r,pos+1,high);
}
}
int Sort :: Partition(int first, int last)
{
int i = first, j = last, temp;
while (i < j)
{
while (i < j && data[i] <= data[j]) j--; /*右侧扫描*/
if (i < j) {
temp = data[i]; data[i] = data[j]; data[j] = temp; i++;
}
while (i < j && data[i] <= data[j]) i++; /*左侧扫描*/
if (i < j) {
temp = data[i]; data[i] = data[j]; data[j] = temp; j--;
}
}
retutn i; /*i为轴值记录的最终位置*/
}
void Sort :: QuickSort(int first, int last)
{
if (first == last) return; /*区间长度为1,递归结束*/
else {
int pivot = Partition(first, last);
QuickSort(first, pivot-1);
QuickSort(pivot+1, last);
}
}
选择类排序算法的基本思想
选择类排序算法的基本思想是从个元素中选出一个最大(小)元素,把它调到序列末(前)端,
简单选择排序的基本思想:第 i 趟(1≤i≤n-1)排序在待排序序列r[i]~r[n] 中选取最小记录,并和第 i 个记录交换。
for (i = 0; i < length-1; i++)
{
第 i 趟简单选择排序;
}
index = i;
for (j = i + 1; j < length; j++)
if (data[j] < data[index]) index = j;
if (index != i) {
交换data[i]和data[index];
}
void Sort :: SelectSort( )
{
int i, j, index, temp;
for (i = 0; i < length; i++)
{
index = i;
for (j = i + 1; j < length-1; j++)
if (data[j] < data[index]) index = j;
if (index != i) {
temp = data[i]; data[i] = data[index]; data[index] = temp;
}
}
}
简单选择排序过程中需要进行的比较次数与初始状态下待排序的记录序列的排序情况无关。
空间性能:
需要2个辅助存储空间
记录最小位置
数据交换缓存O(1)
稳定性:记录交换是跳跃式进行的,因此简单选择排序是不稳定
是一种按锦标赛的思想进行排序的方法。基本思想与体育比赛时的淘汰赛类似:
首先将n个对象的排序码进行两两比较,得到ceil(n/2)个比较的优胜者(较小),作为第一步比较的结果保留下来;
然后对这ceil(n/2)个对象再进行排序码的两两比较,…,如此重复,直到选出一个排序码最小的对象为止。
一颗包含n个结点的完全二叉树,当二叉树不满时,用关键字为∞的结点填满,选出的最小关键字就是这颗树的根结点。在输出了最小关键字之后,为了选择次小关键字,将最小关键字记录所对应的叶子结点的关键字置为∞。
叶子结点和其兄弟结点的关键字比较,修改从该叶子结点到根结点上各结点的止,则根结点的值被修改为次小的关键字。直到左右的结点输出为止。
二叉树的深度是1og2n +1
叶子结点数目为n
第一轮比较次数为n-1
将最小的数对应的叶子结点改为∞,其路径的结点再进行比较
树形选择排序构成是一颗完全二叉树。
其深度为log2n + 1,其中n为待排序元素个数。
堆调整:在一棵完全二叉树中,根结点的左右子树均是堆,调整根结点使整个完全二叉树成为一个堆的过程。
如何设计函数接口?
由于初始建堆和重建堆均调用此函数,因此,设置形参 k 和 last
void Sort :: Sift(int k, int last)
{
int i, j, temp;
i = k; j =2 * i + 1; // i是被调整结点,j是i的左孩子
while (j <= last) //还没有进行到叶子
{
if (j < last && data[j] < data[j+1]) j++; // j指向左右孩子的较大者
if (data[i] > data[j]) break; //已经是堆
else {
temp = data[i]; data[i] = data[j]; data[j] = temp;
i = j; j = 2 * i + 1; //被调整结点位于结点j的位置
}
}
}
堆排序的基本思想:首先将待排序序列构造成一个大根堆,即选出了堆中所有记录的最大者,将它从堆中移走,并将剩余的记录再调整成堆,这样又找出了次小的记录,以此类推,直到堆中只有一个记录。
第 i 趟堆排序将 r1 ~ ri 调整成大根堆,再将堆顶与 ri 交换
for (i = ceil(length/2) - 1; i >= 0; i--)
Sift(i, length-1); //调整结点 i
void Sort :: Sift (int k, int last) //根结点的编号为 k,最后一个结点的编号为 last
=》=》
void Sort :: HeapSort( )
{
int i, temp;
for (i = ceil(length/2) - 1; i >= 0; i--) //从最后一个分支结点至根结点
Sift(i, length-1) ;
for (i = 1; i < length; i++)
{
temp = data[0]; data[0] = data[length-i]; data[length-i] = temp;
Sift(0, length-i-1); //重建堆
}
}
初始建堆:O(nlog2n)
重建堆次数:n-1
重建堆:O(log2i)
最好、最坏、平均情况:O(nlog2n)
空间性能:O(1)
稳定性:不稳定
二路归并排序的基本思想:将待排序序列划分为两个长度相等的子序列,分别对这两个子序列进行排序,得到两个有序子序列,再将这两个有序子序列合并成一个有序序列。
void Sort :: Merge(int first1, int last1, int last2)
{
int *temp = new int[length];
int i = first1, j = last1 + 1, k = first1;
while (i <= last1 && j <= last2)
{
if (data[i] <= data[j]) temp[k++] = data[i++];
else temp[k++] = data[j++];
}
while (i <= last1)
temp[k++] = data[i++];
while (j <= last2)
temp[k++] = data[j++];
for (i = first1; i <= last2; i++)
data[i] = temp[i];
delete[ ] temp;
}
void Sort :: MergeSort1(int first, int last)
{
if (first == last) return;
else {
int mid = (first + last)/2;
MergeSort1(first, mid);
MergeSort1(mid+1, last);
Merge(first, mid, last);
}
}
while (i + 2 * h <= length)
{
Merge(i, i+h-1, i+2*h-1);
i = i + 2 * h;
}
情况 2:i+h<n,则相邻有序子序列一个长度为 h,另一个长度小于 h
if (i + h < length)
Merge(i, i+h-1, length-1);
情况 3:i +h ≥ n,则表明只剩下一个有序子序列
void Sort :: MergePass(int h)
{
int i = 0;
while (i + 2 * h <= length) //有两个长度为h的子序列
{
Merge(i, i+h-1, i+2*h-1);
i = i + 2 * h;
}
if (i + h < length) //两个子序列一个长度小于h
Merge(i, i+h-1, length-1);
}
void Sort :: MergeSort2( )
{
int h = 1; //初始时子序列长度为1
while (h < length)
{
MergePass(h); //一趟归并排序
h = 2 * h;
}
}
从空间性能看
(1)归并排序的空间复杂度为O(n);
(2)快速排序的空间复杂度为O(log2n)~O(n);
(3)其它排序方法的空间复杂度为O(1)。
从记录本身信息量的大小看
记录本身信息量越大,占用的存储空间就越多,移动记录所花费的时间就越多,所以对记录的移动次数较多的算法不利。
记录本身信息量的大小对改进算法的影响不大。
记录个数不多且记录本身的信息量较大时,首选简单选择排序算法