数据结构学习笔记——交换排序(冒泡排序和快速排序)

简介: 数据结构学习笔记——交换排序(冒泡排序和快速排序)

一、交换排序的概念


交换排序通过两两比较待排序的元素,若不满足排序要求则进行交换,直到整个序列有序为止。


二、冒泡排序


(一)排序思想


按照一定的次序(从前往后或从后往前,对应递减和递增)两两比较相邻的元素,若为逆序(r[i-1]<r[i]或r[i]>r[i+1]),则进行交换,直到整个序列都比较完结束,即第一趟冒泡排序结束【第一趟冒泡排序后有一个最小或最大的元素放在排序的最终位置】。然后,继续进行下一趟冒泡排序,之前确定的最小或最大元素则不参与排序比较,第二趟后同样,序列中有一个最小或最大的元素放在排序的最终位置,依次进行下去……,直到整个序列整体有序,若某一趟冒泡排序中没有发生元素交换,则说明此时序列已整体有序,排序后递增的冒泡排序的具体代码如下:

/*冒泡排序(序列结果递增)*/
void BubbleSort1(int r[],int n) {
  for(int i=0; i<n-1; i++) {
  bool flag=false;  //flag变量用于标识本趟冒泡排序是否发生交换 
  for(int j=n-1; j>i; j--) {
    if(r[j-1]>r[j]) { //若为逆序 
    int temp=r[j-1];  //两个元素进行交换 
    r[j-1]=r[j];
    r[j]=temp;
    flag=true;  //前一趟确定的元素不再参与下一趟冒泡排序 
    }
  }
  if(flag==false)//由于未发生交换,说明排序序列已经有序,算法提前结束 
    return;
  }
}


同样也可以写出递减的冒泡排序的代码:

/*冒泡排序(序列结果递减)*/
void BubbleSort2(int r[],int n) {
  for(int i=1; i<n; i++) {
  bool flag=false;  //flag变量用于标识本趟冒泡排序是否发生交换 
  for(int j=1; j<=n-i; j++) {
    if(r[j]>r[j-1]) { //若为逆序 
    int temp=r[j-1];  //两个元素进行交换 
    r[j-1]=r[j];
    r[j]=temp;
    flag=true;  //前一趟确定的元素不再参与下一趟冒泡排序 
    }
  }
  if(flag==false)//由于未发生交换,说明排序序列已经有序,算法提前结束 
    return;
  }
}


例如,对于一个序列{-7,0,97,25,64,11}进行冒泡排序(升序和降序),代码如下:

#include<stdio.h>
#define MAXSIZE 100
/*创建函数*/
void Create(int r[],int n) {
  for(int i=0; i<n; i++) {
  printf("输入第%d个元素:",i+1);
  scanf("%d",&r[i]);
  }
}
/*输出函数*/
void Display(int r[],int n) {
  for(int i=0; i<n; i++)
  printf("%d ",r[i]);
}
/*冒泡排序(序列结果递增)*/
void BubbleSort1(int r[],int n) {
  for(int i=0; i<n-1; i++) {
  bool flag=false;  //flag变量用于标识本趟冒泡排序是否发生交换
  for(int j=n-1; j>i; j--) {
    if(r[j-1]>r[j]) { //若为逆序
    int temp=r[j-1];  //两个元素进行交换
    r[j-1]=r[j];
    r[j]=temp;
    flag=true;  //前一趟确定的元素不再参与下一趟冒泡排序
    }
  }
  if(flag==false)//由于未发生交换,说明排序序列已经有序,算法提前结束
    return;
  }
}
/*冒泡排序(序列结果递减)*/
void BubbleSort2(int r[],int n) {
  for(int i=1; i<n; i++) {
  bool flag=false;  //flag变量用于标识本趟冒泡排序是否发生交换
  for(int j=1; j<=n-i; j++) {
    if(r[j]>r[j-1]) { //若为逆序
    int temp=r[j-1];  //两个元素进行交换
    r[j-1]=r[j];
    r[j]=temp;
    flag=true;  //前一趟确定的元素不再参与下一趟冒泡排序
    }
  }
  if(flag==false)//由于未发生交换,说明排序序列已经有序,算法提前结束
    return;
  }
}
/*主函数*/
int main() {
  int n;
  int r[MAXSIZE];
  printf("请输入排序表的长度:");
  scanf("%d",&n);
  Create(r,n);
  printf("已建立的序列为:\n");
  Display(r,n);
  BubbleSort1(r,n);
  printf("\n");
  printf("递增冒泡排序后的序列为:\n");
  Display(r,n);
  BubbleSort2(r,n);
  printf("\n");
  printf("递减冒泡排序后的序列为:\n");
  Display(r,n);
}


运行结果如下:

1667297731432.jpg

冒泡排序可以用于链式存储结构,具体代码如下:


以下代码参考链接:https://blog.csdn.net/weixin_43638873/article/details/115220993


用于链式存储结构的单链表的冒泡排序(递增)代码如下:

/*冒泡排序(递增)*/
void BubbleSortList1(LinkList L) {
  LNode *s,*r=NULL;
  while(1){
  if(L->next==r)
    return;
  for(s=L->next; s->next!=r; s=s->next) {
    if(s->data>s->next->data) {  //若为逆序
    int temp=s->data; //两个元素进行交换
    s->data=s->next->data;
    s->next->data=temp;
    }
  }
  r=s;  //使r和s的指针结点位置相同
  } 
}


用于链式存储结构的单链表的冒泡排序(递减)代码如下,只需更改若为逆序处的代码:

s->data<s->next->data


完整代码如下:

/*冒泡排序(递减)*/
void BubbleSortList2(LinkList L) {
  LNode *s,*r=NULL;
  while(1) {
  if(L->next==r)  //直至单链表整体有序 
    return;
  for(s=L->next; s->next!=r; s=s->next) {
    if(s->data<s->next->data) {  //若为逆序
    int temp=s->data; //两个元素进行交换
    s->data=s->next->data;
    s->next->data=temp;
    }
  }
  r=s;  //使r和s的指针结点位置相同
  }
}


例如,对于一个序列{2,0,-9,24,3,11,1},采用链式存储结构的单链表,且通过冒泡排序升序和降序排序:

#include<stdio.h>
#include<stdlib.h>
typedef struct LNode {
  int data;
  struct LNode *next;
} LNode,*LinkList;
/*1、初始化一个带头结点的空单链表*/
bool H_InitList(LinkList &L) {
  L=(LNode *)malloc(sizeof(LNode));  //分配一个头结点
  if(L==NULL)  //内存不足,分配失败
  return false;
  L->next=NULL; //头结点之后暂时还没有任何结点,表示空链表
  return true;
}
/*2、尾插法建立单链表(带头结点)*/
void H_CreateTail(LinkList L,int n) {
  LNode *last=L;
  for(int i=0; i<n; i++) {
  int number=i+1;
  printf("请输入第%d个整数:\n",number);
  LNode *s=(LNode *)malloc(sizeof(LNode));  //申请一个新结点s
  scanf("%d",&s->data);   //将数据读入至新结点s的数据域中
  s->next=NULL; //将新结点s的指针域置为空,即空指针NULL
  last->next=s;  //将新结点s插入至单链表的表尾,即last的指针域(末尾结点的后面)
  last=s;  //然后将last指针指向单链表的末尾结点,即指向新结点的后面
  }
}
/*3、单链表(带头结点)的输出*/
void H_DispList(LinkList L) {
  LNode *p;
  p=L->next;
  while(p!=NULL) {
  printf("%d ",p->data);
  p=p->next;
  }
}
/*4、冒泡排序(递增)*/
void BubbleSortList1(LinkList L) {
  LNode *s,*r=NULL;
  while(1){
  if(L->next==r)
    return;
  for(s=L->next; s->next!=r; s=s->next) {
    if(s->data>s->next->data) {  //若为逆序
    int temp=s->data; //两个元素进行交换
    s->data=s->next->data;
    s->next->data=temp;
    }
  }
  r=s;  //使r和s的指针结点位置相同
  } 
}
/*5、冒泡排序(递减)*/
void BubbleSortList2(LinkList L) {
  LNode *s,*r=NULL;
  while(1) {
  if(L->next==r)  //直至单链表整体有序 
    return;
  for(s=L->next; s->next!=r; s=s->next) {
    if(s->data<s->next->data) {  //若为逆序
    int temp=s->data; //两个元素进行交换
    s->data=s->next->data;
    s->next->data=temp;
    }
  }
  r=s;  //使r和s的指针结点位置相同
  }
}
/*主函数*/
int main() {
  LinkList L;  //声明一个指向单链表的指针
  int n,i;
  H_InitList(L);  //初始化一个空的单链表
  printf("请输入要建立单链表的长度:");
  scanf("%d",&n);
  H_CreateTail(L,n);
  printf("建立的单链表如下:\n");
  H_DispList(L);
  printf("\n");
  printf("冒泡排序(递增)后的单链表如下:\n");
  BubbleSortList1(L);
  H_DispList(L);
  printf("\n");
  printf("冒泡排序(递减)后的单链表如下:\n");
  BubbleSortList2(L);
  H_DispList(L);
}


运行结果如下:

1667297793109.jpg


(二)算法分析


分析:

(1)冒泡排序的结束条件是一趟冒泡排序中没有发生元素交换,次数说明整个序列已经整体有序,从而结束算法;

(2)空间复杂度:由于额外辅助空间只有一个temp变量,为参数级,所以冒泡排序的空间复杂度为O(1);

(3)时间复杂度:最好情况下,即待排序结果恰好是排序后的结果,此时比较次数为n-1,移动次数和交换次数都为0,最好时间复杂度为O(n);而最坏情况下,即排好的序列刚好与初始序列相反,呈逆序排列,则此时需要进行n-1趟排序,第i趟排序中要进行n-i次比较,即比较次数=交换次数=n(n-1)/2,由于每次交换都会移动3次元素从而来交换元素,即移动次数为3n(n-1)/2,最坏时间复杂度为O(n2),而考虑平均情况下,故冒泡排序的时间复杂度为O(n2);

(4)稳定性:由于只会在某特定情况才会发生交换,所以冒泡排序是一个稳定的排序算法。

(5)适用性:冒泡排序可适用于顺序存储和链式存储的线性表,当链式存储时,可以从前向后进行比较查找插入位置。

(6)排序方式:冒泡排序是一种内部排序(In-place)。


二、快速排序


(一)排序思想


快速排序是对冒泡排序的一种改进算法,它又称为分区交换排序,通过多次划分操作来实现排序思想。每一趟排序中选取一个关键字作为枢轴,枢轴将待排序的序列分为两个部分,比枢轴小的元素移到其前,比枢轴大的元素移到其后,这是一趟快速排序,然后分别对两个部分按照枢轴划分规则继续进行排序,直至每个区域只有一个元素为止,最后达到整个序列有序。快速排序的思想是递归,其递归进行需要栈来辅助,代码如下:

/*快速排序*/
void QuickSort1(int r[],int low,int high) {
  int temp,i=low,j=high;
  temp=r[i];  //将其设为枢轴,对序列进行划分
  while(i<j) {
  while(i<j&&r[j]>=temp)  //从右往左寻找,找到小于temp的关键字
    j--;
  r[i]=r[j];  //放在枢轴temp的左边
  while(i<j&&r[i]<=temp)  //从左往右寻找,找到大于temp的关键字
    i++;
  r[j]=r[i];  //放在枢轴temp的右边
  }
  r[i]=temp;  //一趟快速排序结束,枢轴temp被放到其最终位置
  if(low<i-1)
  QuickSort1(r,low,i-1);  //递归,对枢轴temp的左边区域进行快速排序
  if(i+1<high)
  QuickSort1(r,i+1,high); //递归,对枢轴temp的右边区域进行快速排序
}


(二)算法分析


分析:

(1)每一个序列的划分算作一趟快速排序,且每趟结束后有一个关键字到达最终位置,即枢轴元素;


这里总结一下,各种排序算法中,

每一趟排序算法的进行都能确定一个元素处于其最终位置的排序算法有以下:

①冒泡排序③简单选择排序②堆排序④快速排序

前三者能形成整体有序的子序列,而后者快速排序只确定枢轴元素的最终位置。


(2)空间复杂度:由于快速排序代码中的递归进行需要栈来辅助,所以其需要的空间较大。其空间复杂度与递归层数(栈的深度)有关,为O(递归层数);


若将n个要排序的元素组成一个二叉树,

这个二叉树的层数就是递归调用的层数(栈的深度),

由于在n个结点的二叉树中,其最小高度=⌊ log2n ⌋

(以2为底n的对数然后再向上取整,取比自己大的最小整数),

其最大高度=n。


所以,可知最好情况下,即最小高度,为⌊ log2n ⌋,最好空间复杂度为O(log2n);而最坏情况下,即最大高度,为n层,最坏空间复杂度为O(n);故平均情况下,为O(log2n);

(3)时间复杂度:其时间复杂度与递归层数有关,为O(n×递归层数),即取决于递归深度,若每次划分越均匀,则递归深度越低;越不均匀,则递归深度越深。在最好情况下,即每次划分比较均匀的情况,最好时间复杂度为O(nlog2n);而最坏情况下,即初始序列有序或逆序时,最坏时间复杂度为O(n2);故平均情况下,为O(nlog2n);


可知,当初始序列有序或逆序时,快速排序的性能最差,

其每次选择的都是最靠序列两边的元素,所划分的区域有一边为空,

所以待排序序列越接近无序或基本上无序,此时算法效率越高;

越接近有序或基本上有序,算法效率越低。


【由于平均情况下的所需时间与最好情况下接近,与最坏情况相比较远,所以快速排序是所有内部排序算法中平均性能最优的排序算法】

(4)稳定性:快速排序是一个不稳定的排序算法;

(6)适用性:适用于顺序存储结构,而不适用于链式存储结构;

(7)排序方式:快速排序是一种内部排序(In-place)。


三、总结


两种交换排序与前面的插入排序的总结如下表:

排序算法 空间复杂度 平均时间复杂度 最好时间复杂度 最坏时间复杂度 排序方式 稳定性 适用性
直接插入排序 O(1) O(n2) O(n) O(n2) 内部排序(In-place) 顺序存储和链式存储
折半插入排序 O(1) O(n2) O(nlog2n) O(n2) 内部排序(In-place) 顺序存储
希尔排序 O(1) 依赖于增量序列 依赖于增量序列 依赖于增量序列 内部排序(In-place) × 顺序存储
冒泡排序 O(1) O(n2) O(n) O(n2) 内部排序(In-place) 顺序存储和链式存储
快速排序 最好情况为O(log2n),最坏情况为O(n) O(nlog2n) O(nlog2n) O(n2) 内部排序(In-place) × 顺序存储


相关文章
|
6月前
|
算法 搜索推荐
快速排序-数据结构与算法
快速排序(Quick Sort)是一种基于分治法的高效排序算法。其核心思想是通过选择基准(pivot),将数组划分为左右两部分,使得左侧元素均小于基准,右侧元素均大于基准,然后递归地对左右两部分进行排序。时间复杂度平均为 O(n log n),最坏情况下为 O(n²)(如数组已有序)。空间复杂度为 O(1),属于原地排序,但稳定性不佳。 实现步骤包括编写 `partition` 核心逻辑、递归调用的 `quickSort` 和辅助函数 `swap`。优化方法有随机化基准和三数取中法,以减少最坏情况的发生。
356 13
|
8月前
|
存储 机器学习/深度学习 算法
C 408—《数据结构》图、查找、排序专题考点(含解析)
408考研——《数据结构》图,查找和排序专题考点选择题汇总(含解析)。
301 29
|
9月前
|
搜索推荐 C++
【C++数据结构——内排序】快速排序(头歌实践教学平台习题)【合集】
快速排序是一种高效的排序算法,基于分治策略。它的主要思想是通过选择一个基准元素(pivot),将数组划分成两部分。一部分的元素都小于等于基准元素,另一部分的元素都大于等于基准元素。然后对这两部分分别进行排序,最终使整个数组有序。(第一行是元素个数,第二行是待排序的原始关键字数据。本关任务:实现快速排序算法。开始你的任务吧,祝你成功!
206 7
|
11月前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
898 9
|
11月前
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
232 59
|
4月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
65 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
9月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
347 77
|
8月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
176 11
|
8月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。