文章目录
常见的几种排序算法
一、冒泡排序
1.排序思想
最经典的当然是冒泡排序了,冒泡排序一共会进行数字总数-1轮排序,其中每一轮的目的就是把最大值给放到最后(升序是这样,当然降序就刚好相反嘛),然后每过一轮,下一轮需要比较的数就会少一个(每一轮都把最大的数放到了最后,下一轮就不需要去比较它了)。冒泡排序的思想大概就是这个样子。如果文字没有看懂,就来看看动画吧,会更加浅显一点。
2.代码实现
因为我的主方向是Java嘛,所以就用Java来做一个演示
import java.util.Arrays;
/**
* 冒泡排序
*/
public class BubbleSort {
public static void main(String[] args) {
int []arr = {3,23,4,6,56,343};
int temp;
boolean flag = false; //判断是否有交换
for (int i = 1; i < arr.length; i++) { //只需要把总数减一个数排序,所以循环总数减一个次
for (int j = 0; j < arr.length - i; j++) { //每执行一次循环,比较次数减一
if (arr[j] > arr[j+1]){
flag = true;
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
if (!flag){
break; //如果发现没有交换就结束循环(说明已经排序好了),一个简单的项目调优
}
else {
flag = false;
}
}
System.out.println(Arrays.toString(arr));
}
}
3.算法复杂度分析
最好的情况:当数组为升序时,只进行了一轮判断,所以时间复杂度为O(n)
最坏情况:当数组为降序时,两个嵌套for循环,很显然时间复杂度为O(n2)
通常来说,只要算法不涉及到动态分配的空间,以及递归、栈所需的空间,空间复杂度通常为0(1)。一个一维数组a[n]
,空间复杂度O(n),二维数组为O(n^2)。其实通俗一点就是,这个排序是否用到了其他的东西来存你的数据(像冒泡在我的案例中用了temp这个变量),所以空间复杂度为O(1)
二、插入排序
1.排序思想
插入排序的思想就是把各个元素先分为两个组,最开始的时候,第一个的元素为一组(有序组),其他的元素(按索引顺序排列即无序组),然后对无序组进行遍历,把每一个元素按顺序插入到有序组里。
2.代码实现
public static void Insertsort(int []arr){
int InsertVal = 0; //当前需要插入的数
int InsertIndex; //当前需要插入的数的前一个对应的索引
for (int i = 1; i < arr.length; i++) {
InsertVal = arr[i]; //从第一个数开始
InsertIndex = i -1;
while (InsertIndex >= 0 && InsertVal < arr[InsertIndex]){
arr[InsertIndex+1] = arr[InsertIndex]; //将插入的数的前一个数赋给插入的数
InsertIndex--;
}
if (InsertIndex != i - 1){ //如果插入的数刚好为最大的就不需要进行赋值
arr[InsertIndex+1] = InsertVal; //将插入的数的值赋给插入的数对应的索引值
}
}
}
3.算法复杂度分析
最好情况:就是当元素为升序,此时只会进行一个for循环,和while、if判断,所以复杂度为O(n)
最坏情况:就是当元素为降序,此时会执行一个for循环嵌套一个while循环和for循环,所以复杂度为O(n2)
三、希尔排序
1.排序思想
希尔排序其实是插入排序的一个优化,其根本上还是插入排序,希尔排序在进行插入排序之前,做了一系列的调整顺序,就是尽量把较小的数字放到前面。首先会对n个元素进行分组(一般最开始是2/n的组),每个组的元素隔了2/n的位置,然后进行比较,把较小的元素放在前面,然后组数为n/4,进行同样的操作,直到组数为1,进行最后一次插入排序。
2.代码实现
public static void main(String[] args) {
int value;
int index;
int arr[] = {2,3,9,1,4,5};
for (int gap = arr.length/2; gap > 0 ; gap = gap/2) { //直到gap等于1为止
for (int i = gap; i < arr.length; i++) {
value = arr[i]; //接收值
index = i; //接收该值对应的索引
while (index - gap >= 0 && arr[index - gap] > value) {
arr[index] = arr[index - gap];
index -= gap;
}
if (index != i)
arr[index] = value;
}
}
System.out.println(Arrays.toString(arr));
}
3.算法复杂度分析
平均时间复杂度:O(n1.3)
四、快速排序
1.排序思想
快速排序是由冒泡排序改进而得的,基本思想就是在待排序的n个元素中任取一个元素(通常取第一个元素)作为基准,把该元素放入适当位置后,数据序列被此元素划分为两个部分,比这个基准小的元素放到这个基准的前面,大的放到后面,这个过程称为一趟快速排序(即一趟划分)。然后再对刚刚产生的两个部分进行同样的操作(递归),直到每部分只有一个元素或者为空。
2.代码实现
public static int partition(int arr[],int l,int r){ //获取这一趟划分基准的索引
int value = arr[l];
while (l < r){
while (l < r && arr[r] >= value){ //找到比基准小的值
r--;
}
arr[l] = arr[r];
while (l < r && arr[l] <= value){ //找到比基准大的值
l++;
}
arr[r] = arr[l];
}
arr[l] = value;
return l;
}
public static void QuickSort(int arr[],int l,int r){
int i;
if (l < r){ //区间内至少存在两个元素的情况
i = partition(arr, l, r);
System.out.println(i);
QuickSort1(arr,l,i-1); //对左区间进行递归排序
QuickSort1(arr,i+1,r); //对右区间进行递归排序
}
}
3.算法复杂度分析
最好的情况:每一次划分都将n个元素划分为两个长度差不多相同的子区间,也就是说每次划分所取的基准都是当前无序区的中值元素,为O(nlog2n)
最坏情况:O(n2)
平均情况:O(nlog2n)
五、选择排序
1.排序思想
把元素分为两个区间,一个为有序区,一个为无序区,每一趟从待排序的元素中选出最小的元素,顺序放在已排好序的有序区的最后。
2.代码实行
public static void SelectSort(int arr[],int n){
int index,temp;
for (int i = 0; i < n - 1; i++) { //循环次数(n-1)次就行
index = i;
for (int j = i + 1; j < n; j++) {
if (arr[index] > arr[j]){
index = j; //找出最小值对应的索引
}
}
if (index != i){ //如果arr[i]不是最小值则进行交换
temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
}
}
}
3.算法复杂度分析
无论初始数据序列的状态如何,两个for循环一定会执行,所以说最好,最坏,平均情况算法复杂度都为O(n2)
六、堆排序
1.排序思想
首先来介绍一下大顶堆和小顶堆的概念,大顶堆指的是每个节点的值都大于或等于其左右孩子节点的值,小顶堆指的是每个节点的值都小于或等于其左右孩子节点的值。
看了这组动图可能还是有点迷,其实堆排序的思想和选择排序的思想是相差不大的,都是每次选择一个最大值然后放到最后面。只是说堆排序是采用树的思想来找最大值的。这个排序用到了树的一些思想,就是对于一个节点(索引为s),如果它有左孩子,则它的索引就为2*s,这个其实很好理解,就不详细解释了。其实堆排序的步骤主要就是先把数组建立成大顶堆,然后将第一个元素和最后一个元素交换,下一次建立大顶堆的时候就不需要最后一个元素(因为这个元素已经是最大了),然后再把第一个元素(初始堆的最后一个元素)弄到对应的位置,然后再重复上面的步骤,直到只有一个元素为止。
2.代码实现
void Shift(int arr[],int low,int high){
int i = low;
int j = 2*i; //i对应的左孩子
int value = arr[i]; //记录节点的值,用于后面做判断
while (j <= high){
if (j < high && arr[j] < arr[j+1]){ //如果左孩子小于右孩子,就指向右孩子
j++;
}
if (value < arr[j]){ //如果父节点小于孩子节点
arr[i] = arr[j]; //就将孩子节点的值调整到双亲节点上
i = j; //将刚刚上面父亲节点的值和孩子的孩子再进行比较
j = 2*i;
}
else {
break;
}
}
arr[i] = value; //将上面父亲节点的值放到对应的位置
}
void HeapSort(int arr[],int n){
int temp;
for (int i = n/2; i >= 0 ; i--) {
Shift(arr,i,n); //形成大顶堆
}
for (int i = n; i >= 1; i--) {
temp = arr[i];
arr[i] = arr[0];
arr[0] = temp; //交换第一个节点和最后一个节点的值
Shift(arr,0,i-1); //把刚刚最后一个节点再进行建立大顶堆,并且不需要刚刚最大的那个元素再次参与了
}
}
3.算法复杂度分析
堆排序的时间主要由建立初始堆和反复建立初始堆这两部分的时间构成。
最好,最好,平均复杂度都为O(nlog2n)
七、基数排序
1.排序思想
基数排序的思想很特殊,它没有涉及关键字的比较,一直在对元素进行分类,基数排序可以理解为在计数排序的基础之上的一种算法。一般对于需要进行排序的元素来说,他们的个位或者十位百位及以上,都是0-9。所以基数排序的思想就是建立0-9的桶,一共十个桶。然后根据这些元素的个位,十位,百位及以上来归类。可能有人会有疑问,为什么要从个位开始,我假设一个情况,有一个序列19,12,23。如果直接按十位来归类的话,确实大体上是可以确定序列的顺序的,但是对应十位数相同的数来说,就确定不了了,所以先按个位数来归类的目的就是确定十位数相同的元素的顺序。先按个位数来归类,序列就变成了12,23,19,19就顺理成章的跑到了12的后面,基数排序也就完成了。对于这个桶使用链表的方式会更加方便。
2.代码实现
#include <stdio.h>
#define MAXD 5
typedef struct node
{
char data[MAXD]; //MAXD为最大的关键字位数
struct node *next;
}NodeType;
void RadixSort(NodeType * &p,int r,int d);
#include "Heap.h"
void RadixSort(NodeType * &p,int r,int d)
{
NodeType *head[10], *tail[10], *t; //定义各链队的首尾指针
int i,j,k;
for(i = 0; i < d; i++) //从地位到高位依次循环
{
for(j = 0; j < r; j++) //对首尾指针进行初始赋值
{
head[j] = tail[j] = NULL;
}
while(p != NULL)
{
k = p->data[i] - '0'; //找出这个元素对应的桶的位置
if (head[k] == NULL)
{
head[k] = p; tail[k] = p; //让两个指针都指向p
}
else
{
tail[k]->next = p; tail[k] = p; //
}
p = p->next;
}
p = NULL;
for (j = 0; j < r; j++) //重新收回各个元素
{
if(head[j] != NULL)
{
if(p == NULL)
{
p = head[j]; t = tail[j];
}
else
{
t->next = head[j]; t = tail[j];
}
}
}
t->next = NULL;
}
}
3.算法复杂度分析
在基数排序中一共进行了d趟的分配和收集,每一趟分配过程需要扫描所有的节点,而收集过程是按队列进行的,所有一趟的执行时间为O(n+r),因此基数排序的时间复杂度为O(d(n+r))。在基数排序中第一趟排序需要的辅助存储空间为r(创建了r个队列),以后都重复用这些队列,所有空间复杂度为O®。
八、归并排序
归并排序的第一步就是把各个元素分为两部分,然后再分为四部分,直到每个部分只有一个元素为止。第二步就是合并,先就是两个部分排序,然后合并成一个部分,然后再两个部分排序,合并成一个部分,如此反复,得到最终的结果。
2.代码实现
public static void MergeSort(int arr[],int left,int right,int temp[]){
if (left < right){
int mid = (left + right)/2;
MergeSort(arr,left,mid,temp);
MergeSort(arr, mid+1,right,temp);
Merge(arr,left,mid,right,temp);
}
}
/**
* 合并两个有序数组
* @param arr
* @param left
* @param mid
* @param right
* @param temp
*/
public static void Merge(int arr[],int left,int mid,int right,int temp[]){
System.out.println("xxxxxx");
int i = left;
int j = mid + 1;
int t = 0;
while (i <= mid && j <= right){
if (arr[i] <= arr[j]){
temp[t] = arr[i];
t++;
i++;
}
else {
temp[t] = arr[j];
t++;
j++;
}
}
while (i <= mid){
temp[t] = arr[i];
t++;
i++;
}
while (j <= right){
temp[t] = arr[j];
t++;
j++;
}
t = 0;
int tempLeft = left;
System.out.println("tempLeft = "+tempLeft + " right = "+right);
while (tempLeft <= right){
arr[tempLeft] = temp[t];
t++;
tempLeft++;
}
}
3.算法复杂度分析
对于长度为n的排序表,二路归并需要进行log2n趟,每趟归并的时间为O(n),故其时间复杂度为O(nlog2n)。在二路归并的过程中,每次二路归并都需要使用一个辅助数组来暂时存储两个有序子表归并的结果,所有总的空间复杂度为O(n)。
这些就是比较常见的排序算法了,可能由于自身水平问题,会有一些错误,还请大家多多指教。