温习
关于稳定性
- 稳定的排序算法:冒泡排序、插入排序、归并排序和基数排序。
- 不是稳定的排序算法:选择排序(示例【2 3 2 1】)、快速排序(示例【3 1 2 3 5 4】主元2)、希尔排序(示例【】)、堆排序(示例【3 2 3 2】最小堆)。
冒泡排序
冒泡排序算法是最简单的排序算法,它的执行效率最低,但冒泡排序算法既适用于顺序物理结构也适用于链式物理结构
数组的冒泡排序
#include<iostream>
#include<vector>
using namespace std;
void bubbleSort(vector<int>& arr, int size)
{
if (size < 2) return;//一个数,不用排序
for (int i = 0; i < size-1; i++) {
int order = 1;//设置是否交换的变量,如果交换=0
for (int j = 0; j < size-1 - i; j++) {
if (arr[j] > arr[j + 1])
{
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
order = 0;//交换 置0
}
}
if (order) break;//一次交换都没有发生,说明序列已经有序
}
}
单向链表的冒泡排序
#include<iostream>
#include<vector>
using namespace std;
struct ListNode{
int value;
struct ListNode* next;
ListNode(int x):value(x),next(nullptr){}
};
//1.0 单链表冒泡排序
//关键在于怎么控制外循环的次数
//一种方案是遍历一遍单链表得到长度,然后类比数组的冒泡排序,用长度控制外循环
//另一种方案是用双指针cur移动,tail记录尾部,每循环一次,tail前移一位
ListNode* listBubbleSort(ListNode* head){
if(!head||!head->next) return head;
ListNode* cur=head;
ListNode* tail=nullptr;
bool flag=false;//标记,有助于已有序时提前跳出循环
int temp=0;
//外循环,循环链表长度-1次
while(head->next!=tail){
//每次循环开始,cur都要从头再来
flag=false;
cur=head;
while(cur->next!=tail){
if(cur->val>cur->next->val){
//前后交换
temp=cur->val;
cur->val=cur->next->val;
cur->next->val=temp;
flag=true;
}
cur=cur->next;
}
//tail前移一位
tail=cur;
//已有序,提前退出循环
if(!flag)
return head;
}
return head;
}
//测试代码
int main(){
vector<ListNode> myVector{ListNode(5),ListNode(2),ListNode(6),ListNode(0),ListNode(1)};
//ptr指向myVector中开始位置的元素
vector<ListNode>::pointer head=myVector.data();//即&myVector[0]
//构造链表
for(int i=0;i<myVector.size()-1;i++){
myVector[i].next=&myVector[i+1];
}
myVector.back().next=nullptr;
head=listBubbleSort(head);
cout<<"after sorted: ";
ListNode* cur=head;
while(cur!=nullptr){
cout<<cur->value<<" ";
cur=cur->next;
}
system("pause");
return 0;
}
选择排序
选择排序可以把数字直接放在最终位置,以位置为中心。
选择排序是给每个位置选择当前元素最小的,具体为每次确定一个位置min,然后找出这个位置到尾部整个区域的最小值,放在min位置,然后后移min。
另一种说法
1、将整个记录序列划分为有序区和无序区,初始时有序区为空,无序区含有待排序所有记录。
2、在无序区中选取关键码最小记录,将它与无序区中的第一个记录交换,使得有序区扩展了一个记录,同时无序区减少了一个记录。
3、不断重复2,直到无序区只剩下一个记录为止。此时所有记录已经按关键码从小到大的顺序排列。
选择排序有多种实现方法,选择排序也可以使用链式物理结构。
选择排序为啥是不稳定的:举例:[2 3 2 1]
参考:https://blog.csdn.net/xiaolangmin/article/details/88538446
数组的选择排序
#include<iostream>
#include<vector>
using namespace std;
//选择排序是固定一个数字,拿它前面所有的数字和它比较。
void selectSort(vector<int>& arr, int size)
{
if (size < 2) return;//一个数,不用排序
for (int i = 0; i < size - 1; i++)
{
int min = i;//记录最小值的下标,从序列首元素开始
for (int j = i+ 1; j < size; j++)//循环确定序列的最小下标
{
if (arr[min] > arr[j])
min = j;
}
if (min != i) {//如果最小下标不是序列首,交换
int temp = arr[i];
arr[i] = arr[min];
arr[min] = temp;
}
}
}
单向链表的选择排序
#include<iostream>
#include<vector>
using namespace std;
struct ListNode{
int value;
struct ListNode* next;
ListNode(int x):value(x),next(nullptr){}
};
//1.1 单链表选择排序
ListNode* listSelectSort(ListNode* head){
if(!head||!head->next) return head;
ListNode* dummy=new ListNode(INT_MIN);
ListNode* tail=dummy;
ListNode* cur=head;
ListNode* temp=nullptr;
ListNode* preMin=nullptr;
ListNode* min=nullptr;
while(cur!=nullptr){
min=cur;
temp=cur;
while(temp->next!=nullptr){
if(temp->next->value<min->value){
preMin=temp;//记录min前一个位置
min=temp->next;
}
temp=temp->next;
}
//下面的操作是为了取出min后维持链表的性质
if(min==cur)//min在头部
cur=cur->next;
else if(min->next==nullptr)//min在尾部
preMin->next=nullptr;
else{//min在中间
preMin->next=min->next;
}
//当前min插到有序部分的尾部
min->next=nullptr;
tail->next=min;
tail=tail->next;
}
head=dummy->next;
delete dummy;
return head;
}
//测试排序结果
int main(){
vector<ListNode> myVector{ListNode(5),ListNode(2),ListNode(6),ListNode(0),ListNode(1)};
//ptr指向myVector中开始位置的元素
vector<ListNode>::pointer head=myVector.data();//即&myVector[0]
//构造链表
for(int i=0;i<myVector.size()-1;i++){
myVector[i].next=&myVector[i+1];
}
myVector.back().next=nullptr;
head=listSelectSort(head);
cout<<"after sorted: ";
ListNode* cur=head;
while(cur!=nullptr){
cout<<cur->value<<" ";
cur=cur->next;
}
system("pause");
return 0;
}
插入排序
插入排序假设前面的数字都已经排序完成了,每次把后面的一个数字插入到前面已经排序完成的数字序列中合适的位置上。
插入排序也可以采用链式物理结构。
数组的插入排序
#include<iostream>
#include<vector>
using namespace std;
void insertSort(vector<int>& arr, int size)
{
int i, j;
for (i = 1; i < size; i++)
{
int insert = arr[i];
for (j = i; j > 0 && insert < arr[j - 1]; j--)
arr[j] = arr[j - 1];
arr[j] = insert;
}
}
单向链表的插入排序
注意单链表插入排序和数组插入排序的不同:数组插入排序是从排好序的部分的最后一个节点往前找,找到第一个比它小的数,然后插到其后面;而单链表只能从前往后遍历,找到第一个比当前节点大的值结束。
插入排序分两种情况,一种是当前节点的值比已经排好序的尾节点的值大,则直接将当前节点挂在已排序的节点即可;
一种是当前节点值比已经排好序的尾节点的值小,则需将已排好序的链表部分从头到尾遍历,找到第一个比当前节点值大的节点,插入到其前面即可。
因为可能待插入的节点可能在第一个节点的前面,因此另外创建一个头结点dummy作为哨兵,指向已经排好序的链表的第一个节点。
//1.2 单链表插入排序
#include<iostream>
#include<vector>
using namespace std;
struct ListNode{
int value;
struct ListNode* next;
ListNode(int x):value(x),next(nullptr){}
};
ListNode* listInsertSort(ListNode* head){
if(!head||!head->next) return head;
//为了便于在头节点的前面插入结点
ListNode* dummy= new ListNode(INT_MIN);
dummy->next=head;
ListNode* tail=head;
ListNode* cur=head->next;
ListNode* temp1=nullptr;
ListNode* temp2=nullptr;
tail->next=nullptr;
//这里有分成了两条链表
//1.dummy开头的保存结果,tail为其尾部
//2.cur开头的链表用于取值
while(cur!=nullptr){
//从dummy指向的链表中找到cur结点插入位置
if(cur->value>=tail->value)//插到tail尾部
{
tail->next=cur;
tail=cur;
cur=cur->next;
tail->next=nullptr;
}else{
temp1=dummy;
while(cur->value > temp1->next->value)
temp1=temp1->next;
//cur插入到temp1后面
temp2=cur->next;
cur->next=temp1->next;
temp1->next=cur;
//cur移到下一个结点
cur=temp2;
}
}
head=dummy->next;
delete dummy;
return head;
}
//测试排序结果
int main(){
vector<ListNode> myVector{ListNode(5),ListNode(2),ListNode(6),ListNode(0),ListNode(1)};
//ptr指向myVector中开始位置的元素
vector<ListNode>::pointer head=myVector.data();//即&myVector[0]
//构造链表
for(int i=0;i<myVector.size()-1;i++){
myVector[i].next=&myVector[i+1];
}
myVector.back().next=nullptr;
head=listInsertSort(head);
cout<<"after sorted: ";
ListNode* cur=head;
while(cur!=nullptr){
cout<<cur->value<<" ";
cur=cur->next;
}
system("pause");
return 0;
}
希尔排序
希尔排序是希尔(Donald Shell)于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序,空间复杂度:O(1)
稳定性:不稳定
基本思想
Shell排序是对插入排序的一种改进。既然插入排序对部分有序的序列很有效,那么我们就要琢磨一下怎样让序列变得部分有序。
Shell排序的思路是,与其像插入排序那样挨个排序,还不如间隔h个元素进行排序,也就是每次排序向前跳h个位置,这样序列虽然整体上看貌似无序,但每间隔h个元素的序列却是交错有序的,这种排序被称为h-排序,而排序后的序列被称为“h-有序的”。
Shell排序有个重要的概念,一个h-有序的序列在g-排序后仍然是h-有序的,那么如果我们以某种方式逐步缩小h直到h变为1,那么当进行h为1的那次排序时,序列已经部分有序,而且排序也退化为一般的插入排序,那么算法的执行效率也就有了提高。
在一开始时,因为h很大,所以子序列很短,随着算法的进行,h越来越小,子序列越来越长,整个序列部分有序的程度越来越高,执行插入排序的效率也就越来越高。
那么h的跳数该怎么选择呢?一个简单实用的“3X+1”即可满足绝大部分的性能要求了。除非遇到巨大的序列,Shell排序还是很快的,在嵌入式和硬件领域应用较为广泛。
希尔排序的时间复杂度和增量序列是相关的
参考希尔排序增量序列简介
- 希尔增量序列 { N / 2 , ( N / 2 ) / 2 , . . . , 1 } \{N/2, (N / 2)/2, ..., 1\} {N/2,(N/2)/2,...,1}这种序列并不是很好的增量序列,使用这个增量序列的时间复杂度(最坏情形)是 O ( n 2 ) O(n^2) O(n2)
- Hibbard提出了另一个增量序列 { 1 , 3 , . . . , 2 k − 1 } \{1, 3, ..., 2^k-1\} {1,3,...,2k−1} ,这种序列的时间复杂度(最坏情形)为 O ( n 1.5 ) O(n^{1.5}) O(n1.5)
- Sedgewick提出了几种增量序列,其中最好的一个序列是 { 1 , 5 , 19 , 41 , 109... } \{1, 5, 19, 41, 109...\} {1,5,19,41,109...},其中项为 m a x ( 9 ∗ 4 j − 9 ∗ 2 j + 1 , 4 k − 3 ∗ 2 k + 1 ) max(9*4^j-9*2^j+1 , 4^k-3*2^k+1) max(9∗4j−9∗2j+1,4k−3∗2k+1)其最坏情形运行时间为 O ( n 1.3 ) O(n^{1.3}) O(n1.3)
- Sedgewick在《算法》一书中使用的增量序列为 “ 3 X + 1 ” = 1 , 4 , 13 , 49 , 121 , 364 , 1093 , … “3X+1”={1,4,13,49, 121, 364, 1093,…} “3X+1”=1,4,13,49,121,364,1093,…,注意,这里的X指序列中的前一个数,跟上面的不一样,比如,13后面的数为3*13+1=40,时间复杂度为 O ( n 1.5 ) O(n^{1.5}) O(n1.5)。这一种简单好用,一般就选它了。
#include<iostream>
#include<vector>
using namespace std;
//vector<int>.operator[] 的声明如下:
//reference operator[] (size_type n);
//const_reference operator[] (size_type n) const;
void swap(int &a,int &b){
int temp=a;
a=b;
b=temp;
}
//增量序列选择3X+1={1,4,13,40,...}
void shellSort(vector<int> &nums){
int size=nums.size();
int h=1;
while(h<size/3){
h=3*h+1;
}
while(h>=1){
//在每个子序列上插入排序
for(int i=h;i<size;i++){
for(int j=i;j>=h;j=j-h){//j从i往前每次跳h远
if(nums[j]>=nums[j-h])//不需要插入排序就跳出
break;
swap(nums[j-h],nums[j]);
}
}
//改变每个子序列的元素下标增量
h=h/3;
}
}
int main(){
vector<int> nums{2,4,23,2,42,4,2,2,2,5,1,6,4,8,9,4,6,7,6,6,4,88,4,8,4,6,4,23,4,2,2,4,2,3};
shellSort(nums);
for(auto& i:nums)
cout<<i<<" ";
system("pause");
return 0;
}