数据结构之排序

内排序探索:冒泡、选择、插入与高效算法详解

数据结构之排序

数据结构相关文章

数据结构之线性表

数据结构之栈和队列

数据结构之二叉树



前言

排序就是按照非递增或者非递减的方式使其成为一个有序的序列。另外需要注意的是,如果排序前两相等的元素之间的相对位置与排序后相比没有发生改变,那就称这个排序是稳定的,反之,其就是不稳定的排序。

根据在排序过程中待排序记录是否被全部放置于内存中分为:内排序和外排序。

内排序是在排序整个过程中,待排序的所有记录全部被放置在内存中。外排序是由于排序的记录个数太多,不能同时放置在内存,整个排序过程需要在内外存之间多次交换数据才能进行。本文讲解的都是内排序。


一、冒泡排序(Bubble Sort)

冒泡排序算法思想:

冒泡排序是一种交换排序,它的基本思想就是:两两比较相邻元素的大小,如果反序则交换,直到没有反序为止。

代码如下:

void Swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}
void BubbleSort(int* arr, int size)
{
    //要比较的趟数
    for (int i = 1; i < size; i++)
    {
        int sign = 1;
        //每一趟冒泡排序的比较
        for (int j = i; j < size; j++)
        {
            if (arr[j] < arr[j - 1])
            {
                Swap(&arr[j], &arr[j - 1]);
                sign = 0;
            }
        }
        //若此趟排序没有移动元素,说明是有序的,不需要再进行之后每一趟的比较了
        if (sign == 1)
        {
            break;
        }
    }
}

冒泡排序的时间复杂度为O(n²)。。

二、简单选择排序(Simple Select Sort)

简单选择排序算法思想:

简单选择排序就是通过比较当前位置及之后的序列中元素的大小,选出相应元素放在当前位置。

代码如下:

void Swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

void SelectSort(int* arr, int size)
{
    //排序趟数
    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 (i != min)
        {
            Swap(&arr[min], arr[i]);
        }
    }
}

简单选择排序的时间复杂度为O(n²)。。

三、直接插入排序(Straight Insertion Sort)

直接插入排序算法思想:

直接插入排序的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的有序序列。

代码如下:

void Swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

void InertSort(int* arr, int size)
{
    for (int i = 1; i < size; i++)
    {
        int end = i;
        while (end > 0)
        {
            if (arr[end] < arr[end - 1])
            {
                Swap(&arr[end], &arr[end - 1]);
                --end;
            }
            else
            {
                break;
            }
        }
    }
}

直接插入排序的时间复杂度是O(n²)。

以上三种排序算法属于简单排序算法,接下来的四种排序算法为改进算法。将为大家重点分析及讲解。

四、希尔排序(Shell Sort)

希尔排序是D.L.Shell于1959年提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是O(n²),希尔排序算法突破这个时间复杂度的第一批算法之一。

我们上面讲到过,直接插入排序,应该说,它的效率在某些时候是很高的,比如在序列本身就是基本就有序的时候,我们只需要少量的插入操作,就可以完成整个序列的排序工作,此时直接插入非常有效。另外,在数据较少时,直接插入排序的优势也比较明显。

可是,这两种条件有些苛刻,不能总指望要排序的序列都是这样的吧!虽然条件有时可能不满足,但是我们要学会去创造条件。于是,大神希尔研究出了一种排序算法,对直接插入排序更改后可以增加效率。

希尔排序算法思想:

我们可以将整个待排序序列分为若干待排序的子序列,对子序列进行直接插入排序,当整个序列基本有序时,在对全体记录进行一次直接插入排序

但是应当注意,子序列不是直接连续的子序列,而应当是跳跃连续的子序列:将相距某个“间隔量”的元素组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。

排序过程如下图所示:
相同颜色的为同一子序列的元素。
在这里插入图片描述
之后我们缩小间隔,再次排序
在这里插入图片描述
最后,对这个基本有序的序列进行直接插入排序。
在这里插入图片描述

代码如下:

void Swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

void ShellSort(int* arr, int size)
{
    int gap = size;
    while (gap >= 1)
    {
        int gap = gap / 3 + 1;
        //要排序子序列个数
        for (int i = 0; i < gap; i += 1)
        {
            //子序列的直接插入排序
            for (j = i + gap; j < size; j += gap)
            {
                int end = j;
                while (end >= gap)
                {
                    if (arr[end] < arr[end - gap])
                    {
                        Swap(&arr[end], &arr[end - gap]);
                    }
                    else
                    {
                        break;
                    }
                }
            }
        } 
    }
}

应当注意,间隔量的最后一次的值必须是一,另外,由于希尔排序是跳跃式移动的,其并不是一种稳定的排序算法。希尔排序的时间复杂度近似为O(n1.5)。

五、堆排序(Heap Sort)

前文说到过简单选择排序,它在待排序的n个元素中选择一个最小或最大的元素需要比较n - 1次,可惜的是,它并由有把每一趟的结果保存下来,因而之后的排序又重复的执行了这些操作。如果可以做到,在每次选择最小元素的同时,并根据比较结果对其他元素进行调整,那么排序的效率就会非常高了。而堆排序就是对简单选择排序的一种改进。

堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大根堆。反之称为小根堆。

堆排序算法思想:

堆排序就是利用堆(这里我们选择大根堆)进行排序的算法。基本思想是:

  1. 将待排序序列构造成一个大根堆。此时,整个序列的最大值就是堆顶的根结点
  2. 将它于堆数组的末尾元素交换,此时末尾元素就是整个数组元素的最大值。然后将剩余的n - 1个元素重复第一步和第二步。

堆的向下调整算法:
图示如下:
首先从最后一个非叶子节点开始向下调整,在依此遍历之前的叶子节点,进行相应操作。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码如下:

void AdjustDown(int* arr, int size, int parent)
{
    int child = parent * 2 + 1;
    while (child < size)
    {
        //选择左右孩子中较大者
        if (child + 1 < size && arr[child + 1] > arr[child])
        {
            ++child;
        }
        //再将较大者于双亲结点比较
        if (arr[child] > arr[parent])
        {   
            Swap(&arr[child], &arr[parent]);
            parent = child;
            child = parent * 2 + 1;
        }
        else
        {
            break;
        }
    }
}

利用向下调整算法实现堆排序
图示如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码如下:

void HeapSort(int* arr, int size)
{
    for (int i = (size - 1 - 1) / 2; i >= 0; --i)
    {
        AdjustDown(arr, size, i);
    }
    int end = size - 1;
    while (end > 0)
    {
        Swap(arr, &arr[end]);
        AdjustDown(arr, end, 0);
        --end;
    }
}

若对双亲结点和子结点的位置关系有疑问,请见数据结构之二叉树

堆排序的时间复杂度是O(nlog2n)。

六、归并排序(Merging Sort)

前面我们讲到了堆排序,因为其利用了完全二叉树的深度是log2n + 1(向下取整)的特性。而归并排序就是一种更加简单且直接利用完全二叉树来排序的。

归并排序算法思想:

假设我们有n个元素,可以将这n个元素看成n个有序的子序列,每个子序列的长度为一,然后两两归并,如此重复直到得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。

如下图所示:
在这里插入图片描述

非递归代码如下:

#include<string.h>
#include<errno.h>

void _MergeSortN(int* arr, int* temp, int start1, int end1, int start2, int end2)
{
	int i = start1;
	int start = start1;
	while (start1 <= end1 && start2 <= end2)
	{
		if (arr[start1] > arr[start2])
		{
			temp[i] = arr[start2++];
		}
		else
		{
			temp[i] = arr[start1++];
		}
		++i;
	}

	while (start1 <= end1)
	{
		temp[i++] = arr[start1++];
	}
	while (start2 <= end2)
	{
		temp[i++] = arr[start2++];
	}

	for (i = start; i <= end2; i++)
	{
		arr[i] = temp[i];
	}
}
void MergeSortN(int* arr, int size)
{
	int* temp = (int*)malloc(sizeof(int) * size);
	if (temp == NULL)
	{
		printf("%s\n", strerror(errno));
		exit(-1);
	}
	int gap = 1;
	while (gap < size)
	{
		for (int i = 0; i < size; i += gap * 2)
		{
			if (i + gap >= size)
			{
				break;
			}
			if (i + gap * 2 - 1 >= size)
			{
				_MergeSortN(arr, temp, i, i + gap - 1, i + gap, size - 1);
			}
			else
			{
				_MergeSortN(arr, temp, i, i + gap - 1, i + gap, i + 2 * gap - 1);
			}
		}

		gap *= 2;
	}

	free(temp);
	temp = NULL;
}

在这里插入图片描述

递归代码如下:

#include<string.h>
#include<errno.h>

void _MergeSort(int* arr, int* temp, int start, int end)
{
    if (start >= end)
    {
        return;
    }
    int mid = (start + end) >> 1;
    _MergeSort(arr, temp, start, mid);
    _MergeSort(arr, temp, mid + 1, end);
    int i = start;
    int start1 = start;
    int start2 = mid +  1;
    while (start1 <= mid && start2 <= end)
    {
        if (arr[start1] > arr[start2])
        {
            temp[i++] = arr[start2++];
        }
        else
        {
            temp[i++] = arr[start1++];
        }
    }

    while (start1 <= mid)
    {
        temp[i++] = arr[start1++];
    }
    while (start2 <= end)
    {
        temp[i++] = arr[start2++];
    }

    for (i = start; i <= end; i++)
    {
        arr[i] = temp[i];
    }

}

void MergeSort(int* arr, int size)
{
    int* temp = (int*)malloc(sizeof(int) * size);
    if (temp == NULL)
    {
        printf("%s\n", strerror(errno));
        exit(-1);
    }
    _MergeSort(arr, temp, 0, size - 1);

    free(temp);
    temp = NULL;
    
}

归并排序时间复杂度为O(nlog2n)。

七、快速排序(Quick Sort)

希尔排序相当于直接插入排序的升级,它们同属于插入排序类,堆排序相当于简单选择排序的升级,它们同属于选择排序类。而快速排序其实就是我们前面认为最慢的冒泡排序的升级,它们都属于交换类排序。即它也是通过不断地比较和移动交换来实现排序的,只不过它的实现增大了序列的比较和移动的距离,将元素较大的从前直接移动到后面,将元素较小的直接从后移动到前面,从而减少了总的比较次数和移动交换次数。

快速排序算法思想

通过一趟排序将待排序序列分割成独立的两部分,其中一部分的元素均小于或大于另一部分元素,然后再分别对这两部分元素再进行分隔和排序,以达到整个序列有序的目的。

下面将介绍快排的三种方法:

  1. hoare

首先取序列最左侧元素为key,然后让两个指针一个先从序列右侧找小于key的元素,找到之后再让另一个指针从序列左侧找到大于key的元素,交换两元素位置,直到二者相遇。

如下图所示:

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

代码如下:

int GetMinIndex(int* arr, int start, int end)
{
    int mid = (start + end) / 2;
    if (arr[start] > arr[end])
    {
        if (arr[mid] > arr[start])
        {
            return start;
        }
        else
        {
            if (arr[mid] > arr[end])
            {
                return mid;
            }
            else
            {
                return end;
            }
        }
    }
    else
    {
        if (arr[mid] > arr[end])
        {
            return end;
        }
        else
        {
            if (arr[mid] > arr[start])
            {
                return mid;
            }
            else
            {
                return start;
            }
        }
    }
}

void Swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int PartSort(int* arr, int start, int end)
{
    int mid = GetMinIndex(arr, start, end);
    Swap(&arr[mid], &arr[start]);
    int key = arr[start];
    int left = start;
    int right = end;

    while (left< right)
    {
        while (left < right && arr[right] >= key)
        {
            ++right;
        }
        while (left < right && arr[left] <= key)
        {
            ++left;
        }
        Swap(&arr[left], &arr[right]);
    }
    Swap(&arr[start], &arr[left]);
    return left;
}
void QuickSort(int* arr, int start, int end)
{
    if (start >= end)
    {
        return;
    }

    int mid = PartSort(arr, start, end);
    QuickSort(arr, start, mid);
    QuickSort(arr, mid + 1, end);

}

为了防止出现快速排序的最差情况,加入了三数取中的优化。

  1. 挖坑法

代码如下:


int PartSort(int* arr, int start, int end)
{
    int mid = GetMinIndex(arr, start, end);
    Swap(&arr[start], &arr[mid]);
    int key = arr[start];
    int left = start;
    int right = end;

    while (left < right)
    {
        while (left < right && arr[right] >= key)
        {
            --right;
        }
        arr[left] = arr[right];
        while (left < right && arr[left] <= key)
        {
            ++left;
        }
        arr[right] = arr[left];
    }
    arr[left] = key;
    return left;
}

void QuickSort(int* arr, int start, int end)
{
    if (start >= end)
    {
        return;
    }

    int mid = PartSort(arr, start, end);
    QuickSort(arr, start, mid);
    QuickSort(arr, mid + 1, end);

}
  1. 前后指针法

代码如下:


int PartSort3(int* arr, int start, int end)
{
    int mid = GetMidIndex(arr, start, end);
    Swap(&arr[start], &arr[mid]);
    int key = arr[start];
    int prev = start;
    int cur = start + 1;

    while (cur <= end)
    {
        if (arr[cur] < key && (++prev) != cur)
        {
            Swap(&arr[prev], &arr[cur]);
        }
        ++cur;
    }

    Swap(&arr[start], &arr[prev]);
    return prev;
}

void QuickSort(int* arr, int start, int end)
{
    if (start >= end)
    {
        return;
    }

    int mid = PartSort(arr, start, end);
    QuickSort(arr, start, mid);
    QuickSort(arr, mid + 1, end);

}

快速排序时间复杂度为O(nlog2n)。


总结

根据排序过程中方借助的主要操作,我们将内排序分为:插入排序、交换排序、选择排序和归并排序四类。
在这里插入图片描述
下面是各种排序算法的各项指标:
在这里插入图片描述
应当注意简单选择排序根据算法情况的不同,也有可能是不稳定的一种算法。

评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值