数据结构学习:排序
排序(Sort),就是重新排列表中的元素,使表中的元素满⾜按关键字有序的过程。
算法的稳定性。若待排序表中有两个元素Ri 和Rj ,其对应的关键字相同即keyi = keyj ,且在排序
前Ri 在Rj 的前⾯,若使⽤某⼀排序算法排序后,Ri 仍然在Rj 的前⾯,则称这个排序算法是稳定
的,否则称排序算法是不稳定的。
排序:
内部排序:数据都在内存中
外部排序:数据太多无法全部放入内存
1.插⼊排序
算法思想:每次将⼀个待排序的记录按其关键字⼤⼩插⼊到前⾯已排好序的⼦序列中,直到全部记录插⼊完成。
代码实现:
// 直接插入排序
void InsertSort(int A[],int n)
{
int i,j,temp;
for(i = 1; i < n; i++) //将各元素插入已排好序的序列
{
if(A[i] < A[i-1]) //若A[i]关键字小于前驱
{
temp = A[i]; //用temp暂存A[i]
for(j = i-1; j >= 0 && A[j] > temp;j--)
A[j+1] = A[j];//所有大于temp的向后移
A[j+1] = temp;
}
}
}
// 直接插入排序(带哨兵)用A[0]来代替temp的作用
void InsertSort(int A[],int n)
{
int i,j;
for(i = 2; i < n; i++) //将各元素插入已排好序的序列
{
if(A[i] < A[i-1]) //若A[i]关键字小于前驱
{
A[0] = A[i]; //用temp暂存A[i]
for(j = i-1; A[j] > A[0];j--)
A[j+1] = A[j];//所有大于temp的向后移
A[j+1] = A[0];
}
}
}
空间复杂度:O(1)
时间复杂度:主要来⾃对⽐关键字、移动元素若有 n 个元素,则需要 n-1 趟处理
最好时间复杂度(全部有序):O(n)
最坏时间复杂度(全部逆序):O(n^2 )
平均时间复杂度:O(n^2)
优化——折半插⼊排序
思路:先⽤折半查找找到应该插⼊的位置,再移动元素
注意:
当 low>high 时折半查找停⽌,应将 [low, i-1] 内的元素全部右移,并将 A[0] 复制到 low 所指位置
当 A[mid]==A[0] 时,为了保证算法的“稳定性”,应继续在 mid 所指位置右边寻找插⼊位置
// 折半插入排序
void InsertSort(int A[],int n)
{
int i,j,low,high,mid;
for(i = 2; i < n; i++) //将各元素插入已排好序的序列
{
A[0] = A[i]; //用temp暂存A[i]
low = 1,high = i-1;
while(low <= high) //折半查找
{
mid = (low + high)/2;
if(A[mid] > A[0])
high = mid - 1;
else
low = mid + 1;
}
for(j = i-1; A[j] >= high + 1;j--)
A[j+1] = A[j];//所有大于temp的向后移
A[high+1] = A[0];
}
}
}
移动元素的次数变少了,但是关键字对⽐的次数依然是O(n^2 ) 数量级,整体来看时间复杂度依然是O(n^2)
2.希尔排序(Shell Sort)
希尔排序:先将待排序表分割成若⼲形如 L[i, i + d, i + 2d,…, i + kd] 的“特殊”⼦表,对各个⼦表分别进⾏直接插⼊排序。缩⼩增量d,重复上述过程,直到d=1为⽌。
Eg:
第一趟结束后:
第二趟结束后:
代码实现:
// 希尔排序
void ShellSort(int A[],int n)
{
int d,i,j;
for(d = n/2;d >= 1; d = d / 2)//改变每一趟的表
{
for(i = d+1; i <= n; i++)
{
if(A[i] < A[i-d])//后面的与他前面第d的个比较
{
A[0] = A[i];
for(j = i-d;j > 0 && A[0] < A[j];j = j - d)
A[j+d] = A[j];
A[j+d] = A[0];
}
}
}
}
注意:A[0]是用来短暂的存储一下元素,不是哨兵
空间复杂度:O(1)
最坏时间复杂度为 O(n^2 )(优于直接插入排序)
稳定性:不稳定!(49,49排序后49,49)
适⽤性:仅适⽤于顺序表,不适⽤于链表
交换排序:
①冒泡排序
②快速排序
基于“交换”的排序:根据序列中两个元素关键字的⽐较结果来对换这两个记录在序列中的位置
3.冒泡排序
从后往前(或从前往后)两两⽐较相邻元素的值,若为逆序(即A[i-1]>A[i]),则交换它们,直到序列⽐较完。称这样过程为“⼀趟”冒泡排序。总共需进⾏ n-1 趟冒泡。
代码实现:
// 交换
void swap(int &a,int &b)
{
int temp = a;
a = b;
b = temp;
}
// 冒泡排序
void BubbleSort(int A[],int n)
{
for(int i = 0;i < n;i++)
{
bool flag = flase; //用来标志本趟是否发生交换
for(int j = n-1;j>i;j--)
{
if(A[j-1]>A[j])
{
swap(A[j-1],A[j])
flag = true;
}
}
if(flag == flase)
return; //本趟遍历后没有发生交换,说明表已有序
}
}
空间复杂度:O(1)
最好时间复杂度=O(n)
最坏时间复杂度=O(n^2 )
平均时间复杂度=O(n^2 )
4.快速排序
算法思想:在待排序表L[1…n]中任取⼀个元素pivot作为枢轴(或基准,通常取⾸元素),通过⼀趟排序将待排序表划
分为独⽴的两部分L[1…k-1]和L[k+1…n],使得L[1…k-1]中的所有元素⼩于pivot,L[k+1…n]中的所有元素⼤于等于
pivot,则pivot放在了其最终位置L(k)上,这个过程称为⼀次“划分”。然后分别递归地对两个⼦表重复上述过程,直⾄每部分内只有⼀个元素或空为⽌,即所有元素放在了其最终位置上
定义low 和 high指针分别指向最低和最高
1.high向前走,发现比选定元素小的,则将其放到 low 指定的位置,然后 high 固定不动,交换移动指针
2.low向后移动,发现比选定元素大的,将其放到 high 指针所指的位置,然后交换移动指针
3.重复1和2 直到low 和 high 重合,然后将选定元素放到重合的位置
4.此时以选定元素为分界线,原表分为两个字表,对两个字表递归使用上述步骤直⾄每部分内只有⼀个元素或空为⽌,即所有元素放在了其最终位置上
代码实现:
// 用一个元素将待排序序列分为两部分
int Partition(int A[],int low,int high)
{
int pivot = A[low];
while(low < high)
{
while(low < high && A[high] >= pivot)
high--;
A[low] = A[high];
while(low < high && A[low] <= pivot)
low++;
A[high] = A[low];
}
A[low] = pivot;
return low;
}
// 快速排序
void QuickSort(int A[],int low, int high)
{
if(low < high) //跳出递归
{
int pivotpos = Partition(A,low,high); //划分
QuickSort(A,low,pivotpos - 1); //划分左字表
QuickSort(A,pivotpos + 1,high); //划分右子表
}
}
空间复杂度=O(递归层数)
时间复杂度=O(n*递归层数)
若每⼀次选中的“枢轴”将待排序序列划分为很不均匀的两个部分,则会导致递归深度增加,算法效率变低
若每⼀次选中的“枢轴”将待排序序列划分为均匀的两个部分,则递归深度最⼩,算法效率最⾼
平均时间复杂度=O(nlog2 n)
稳定性:不稳定
⼀次划分可以确定⼀个元素的最终位置,⽽⼀趟排序也许可以确定多个元素的最终位置。
选择排序:
简单选择排序
堆排序
选择排序:每⼀趟在待排序元素中选取关键字最⼩(或最⼤)的元素加⼊有序⼦序列
5.简单选择排序
每⼀趟在待排序元素中选取关键字最⼩的元素加⼊有序⼦序列
即:遍历一趟,选择n个元素中最小的放到n个元素的开头,然后对剩下的n-1个待排序元素重复操作;
代码实现:
// 简单选择排序
void SelectSort(int A[],int n)
{
for(int i = 0;i < n-1;i++) //共进行n-1趟
{
int min = i; //用来记录最小元素
for(int j = i + 1;j<n; j++) //在待排序元素中找到最小元素
{
if(A[j] < A[min])
min = j; //更新最小元素位置
}
if(min != i) //看找到的最小元素是否是第一个元素
swap(A[i],A[min]);
}
}
空间复杂度:O(1)
时间复杂度=O(n^2 )
稳定性:不稳定
适⽤性:既可以⽤于顺序表,也可⽤于链表
6.堆排序
若n个关键字序列L[1…n] 满⾜下⾯某⼀条性质,则称为堆(Heap):
① 若满⾜:L(i)≥L(2i)且L(i)≥L(2i+1) (1 ≤ i ≤n/2 )—— ⼤根堆(⼤顶堆)
② 若满⾜:L(i)≤L(2i)且L(i)≤L(2i+1) (1 ≤ i ≤n/2 )—— ⼩根堆(⼩顶堆)
⼤根堆:完全⼆叉树中,根≥左、右
⼩根堆:完全⼆叉树中,根≤左、右
建⽴⼤根堆
把所有⾮终端结点都检查⼀遍,是否满⾜⼤根堆的要求,如果不满⾜,则进⾏调整
检查当前结点是否满⾜ 根≥左、右若不满⾜,将当前结点与更⼤的⼀个孩⼦互换
若元素互换破坏了下⼀级的堆,则采⽤相同的⽅法继续往下调整(⼩元素不断“下坠”)
基于⼤根堆进⾏排序
代码实现:
// 将以k为根的子树调整为大根堆
void HeadAdjust(int A[],int k,int len)
{
A[0] = A[k]; //A[0]暂存子树的根节点
for(int i = 2*k;i<=len;i = i*2) //沿k较大的子节点向下筛选
{
if(i < len&& A[i] < A[i+1]) //取k较大的子节点下标
i++; //若左右孩一样大,则优先和左孩子交换
if(A[0] >= A[i]) //筛选结束
break;
else
{
A[k] = A[i]; //将A[i]调整到双亲结点上
k = i; //修改k值,便于继续向下寻找
}
}
A[k] = A[0]; //被筛选的结点值放到最终位置
}
// 建立大根堆
void BuildMaxHeap(int A[],int len)
{
for(int i = len/2;i>0;i--) //从后往前调整所有非终端结点
HeadAdjust(A,i,len);
}
// 堆排序的完整逻辑
void HeadSort(int A[],int len)
{
BuildMaxHeap(A,len); //初始建堆
for(int i = len;i > 1;i--) //n-1趟的交换和建堆过程
{
swap(A[i],A[1]); //堆顶元素和堆底元素交换
HeadAdjust(A,1,i-1); //剩余的待排序元素整理成堆
}
}
堆排序的时间复杂度 = O(nlog2 n)
堆排序的空间复杂度 = O(1)
堆排序是不稳定的
1.在堆中插⼊新元素
对于⼩根堆,新元素放到表尾,与⽗节点对⽐,若新元素⽐⽗节点更⼩,则将⼆者互换。新元素就这样⼀路“上升”,直到⽆法继续上升为⽌
2.在堆中删除元素
被删除的元素⽤堆底元素替代,然后让该元素不断“下坠”,直到⽆法下坠为⽌