常见的排序算法汇总(详解篇)

news2024/11/16 19:38:13

目录

排序的概念以及运用

排序的概念

1. 插入排序

1.1 直接插入排序

1.1.1 基本思想

1.1.2 代码实现

直接插入排序的特征总结:

1.1.3 希尔排序(缩小增量排序)🚀

1.1.4基本思想🚀

1.1.5 代码实现🚀

希尔排序的特征总结:

2.选择排序

2.1直接选择排序

2.1.1 基本思想

2.1.2 代码实现

直接选择排序的特征总结:

2.1.3 堆排序🚀

2.1.4基本思想🚀

2.1.5 代码实现🚀

直接选择排序的特征总结:

3.交换排序

3.1冒泡排序

3.1.1基本思想

3.1.2代码实现

直接选择排序的特征总结:

3.1.3 快速排序(重点)🚀(未完成)

3.1.4 实现思路🚀

->1. 快速排序第一种实现思路(Hoare版本)🚀

方法一完整代码实现(Hoare版本)🚀:

->2. 第二种实现思路(挖坑法)🚀

​编辑

方法二完整代码实现🚀:

->3.第三种实现方法(前后指针法)🚀

方法三完整代码实现🚀:

3.1.5 优化快速排序🚀

3.1.6 快速排序的非递归现实🚀

3.1.7快速排序三路划分🚀(解决重复数据)

基本思想🚀:

代码实现🚀:

快速排序的特征总结:

4.归并排序🚀

4.1 归并排序(递归实现)🚀

4.1.1基本思想🚀

4.1.2 代码实现(递归实现):🚀

4.2 归并排序(非递归实现)🚀

代码实现1(一次性拷贝进a数组):🚀

代码实现2(归并一部分就拷贝一部分进数组a):🚀

归并排序的特征总结:

5.非比较排序

5.1计数排序

5.1.1基本思路

5.1.2 计数排序代码实现:

计数排序的特征总结:

6.排序算法复杂度及稳定性分析

7.各个算法效率比较


排序的概念以及运用

排序的概念

排序 :所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性 :假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中, r[i]=r[j] ,且 r[i] 在 r[j] 之前,而在排序后的序列中, r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。

例:


内部排序 :数据元素全部放在内存中的排序。
外部排序 :数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序。

如果我们要放500G的内容,500G内存中肯定是放不下的,这时我们就需要借助外存,将数据存在外存(例:文件)中,如何放呢?用归并的思想

例:

现在我们讲解的算法如图所示

1. 插入排序

1.1 直接插入排序

当插入第 i(i>=1) 个元素时,前面的 array[0],array[1],…,array[i-1] 已经排好序,此时用 array[i] 的排序码与array[i-1],array[i-2],…的排序码顺序进行比较,找到插入位置即将 array[i] 插入,原来位置上的元素顺序后移

1.1.1 基本思想

直接插入排序是一种简单的插入排序法,其基本思想是:

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

在实际中我们玩扑克牌的时候就用到了插入排序的思想

1.1.2 代码实现

//直接插入排序,以升序为例
void InsertSort(int* a, int n)
{
	for(int i = 1; i < n; ++i)
	{ 
		int end = i - 1;
		int tmp = a[i];

		while (end >= 0)
		{
			//将小于tmp的值往后面挪动
			if (tmp < a[end])
			{
				a[end + 1] = a[end];
				--end;
			}
			else
			{
				//不是每次end小于0的情况
				break;
			}
		}
		//将tmp放到,需要插入的位置
		a[end + 1] = tmp;
	}
}

直接插入排序的特征总结:

1.元素集合越接近有序,直接插入排序算法的时间效率越高

2.时间复杂度

最坏情况:O(N^2)

最好情况:O(N)  (就算这组数据是有序的它也得遍历N遍)

时间复杂度一般取最坏情况:O(N^2)

3.空间复杂度:O(1) ,它是一种稳定的排序算法

4.稳定性:稳定

1.1.3 希尔排序(缩小增量排序)🚀

1.1.4基本思想🚀

希尔排序是直接插入排序的改良版,希尔排序又称:缩小增量法。希尔排序的基本思想是:

先选定一个整数,把待排序的文件中的所有记录分成组,所有距离相同的记录分在同一组内,并对每一组内的记录进行排序。然后,重复上述分组和排序的工作。当距离达到1时,所有距离在统一组内排好序

1.1.5 代码实现🚀

//希尔排序
void ShellSort(int* a, int n)
{
    int gap = n;

    while(gap > 1) //这里循环条件不能等于1,不然会死循环
    {
        gap = gap / 3 + 1; //这里使用gap /= 2;也可以

        for(int i = 0; i < n - gap; ++i)
        {
            int end = i;
            int tmp = a[i + gap];
            
            while(end >= 0)
            {
                if(tmp < a[end])
                {
                    a[end + gap] = a[end];
                    end -= gap;
                }
                else
                {
                    break;
                }
            }
            a[end + gap] = tmp;
        }
    }    
    
}

希尔排序的特征总结:

1.希尔排序是对直接插入排序的优化

2.当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时(我们的时间复杂度基就是最好情况:O(N),如果没有进行预排这里就按最坏情况来算),数组已经接近有序了,这样就会很快。这样整体而言,可以达到优化的效果

3.希尔排序的时间复杂度不好计算,因为gap的取值方法有很多,导致很难去计算,因此在好些树中给出的希尔排序的时间复杂度都不固定

所以这里直接给出结论:希尔排序的时间复杂度就按照:O(N^1.3)来记就可以了

4.稳定性:不稳定

2.选择排序

2.1直接选择排序

  • 在元素集合 array[i]--array[n-1] 中选择关键码最大 ( 小 ) 的数据元素
  • 若它不是这组元素中的最后一个 ( 第一个 ) 元素,则将它与这组元素中的最后一个(第一个)元素交换
  • 在剩余的array[i]--array[n-2] ( array[i+1]--array[n-1] )集合中,重复上述步骤,直到集合剩余 1 个元素

2.1.1 基本思想

 每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完

2.1.2 代码实现

//交换
void Swap(int* x, int* y)
{
    int tmp = *x;
    *x = *y;
    *y = tmp;
}

//直接选择排序
void SelectSort(int* a, int n)
{
	int left = 0;
	int right = n - 1;

	int mini, maxi;

	//随便给初值
	while (left < right)
	{
		//每次循环都需要重置mini,maxi,因为是上一次的结果不计入这次循环。
		mini = left, maxi = left; 

		for (int i = left + 1; i <= right; ++i)
		{
			if (a[i] > a[maxi])
			{
				maxi = i;
			}

			if (a[i] < a[mini])
			{
				mini = i;
			}
		}

		Swap(&a[left], &a[mini]);

		//如果left于maxi重叠

		if (left == maxi)
		{
			maxi = mini;
		}

		Swap(&a[right], &a[maxi]);

		left++;
		right--;
	}
}

直接选择排序的特征总结:

1.直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用

2.时间复杂度:

最坏情况:O(N^2)

最好情况:O(N^2)

最好情况也需要将整个数据遍历N^2次,所以这个排序在实际中很少使用

3.空间复杂度:O(1)

4.稳定性:不稳定

2.1.3 堆排序🚀

2.1.4基本思想🚀

总共分为两个步骤

1.建堆

  • 升序:建大堆
  • 降序:建小堆

思路图示:

升序为什么不建小堆?

如果排升序建的是小堆,根结点的数据是最小的,剩下的做堆,选次小的,与上面删除堆尾元素一样,这样做会导致后面父子关系全乱了,得重新排序

如图:

排升序,建大堆第一个元素与最后一个元素交换,然后将最后一个元素抹去(不计入建堆中)其他n - 1个元素建堆,以此类推直到有序即可

2.1.5 代码实现🚀

//向上调整,排升序,建大堆
void AdjustUp(int* a, int child)
{
    assert(a);

    int parent = (child - 1)/2;
    while(child > 0)    
    {
        if(a[parent] < a[child])
        {
            Swap(&a[parent], &a[child]);
            child = parent;
            parent = (child - 1)/2;
        }
        else
        {
            break;
        }
    }
}

//向下调整,建大堆
void AdjustDown(int* a, int n, int parent)
{
    assert(a);

    int child = parent * 2;

    while(child <= n)
    {
        if((child + 1) <= n && a[child + 1] > a[child])
        {
            ++child;
        }

        if(a[parent] < a[child])
        {
            Swap(&a[parent], &a[child]);
            parent = child;
            child = parent * 2;
        }
        else
        {
            break;
        }
    }    
}

//堆排序
void HeapSort(int* a, int n)
{
    int end = n - 1;    

    //向上调整建堆
    /*for(int i = end; i > 0; --i)
    {
        AdjustUp(a,i);
    } */   

    //向下调整建堆
    for(int i = (end - 1)/2; i <= end; --i)
    {
        AdjustDown(a, end + 1, i);
    }

    //排序
    while(end > 0)
    {
        Swap(&a[0], &a[end--]);
        AdjustDown(a, end, 0);
    }
    
}

这里为什么不使用向上建堆?向下排序比向上排序的时间复杂度快一点

向下排序的时间复杂度:O(N)

向上排序的时间复杂度:O(N*logN)

向下排序时间复杂度解析:

向下排序时间复杂度解析:

直接选择排序的特征总结:

1. 堆排序使用堆来排序效率就高了很多

2. 时间复杂度:O(N*logN)

3.空间复杂度:O(1)

4.稳定性:不稳定

3.交换排序

所谓交换,就是根据序列中两个记录键值的比较结果来对换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的记录向序列的前部移动。

3.1冒泡排序

3.1.1基本思想

冒泡排序也称沉底法:以升序为例,每一次执行都会将最大的数沉底

3.1.2代码实现

//冒泡排序
void BubbleSort(int* a, int n)
{
	for (int i = 0; i < n - 1; ++i)
	{
		bool exchange = false;
		for(int j = 1; j < n - i; ++j)
		{
			if (a[j - 1] > a[j])
			{
				Swap(&a[j - 1], &a[j]);
				exchange = true;
			}
		}

		if (!exchange)
		{
			break;
		}
	}
}

直接选择排序的特征总结:

冒泡排序的特征总结:

1.冒泡排序是一种非常容易理解的排序

2.时间复杂度:

最坏情况:O(N^2)

最好情况:O(N)

3.空间复杂度:O(1)

4.稳定性:稳定

3.1.3 快速排序(重点)🚀(未完成)

快速排序是 Hoare 于 1962 年提出的一种二叉树结构的交换排序方法,其基本思想为: 任取待排序元素序列中 的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止 。

3.1.4 实现思路🚀

->1. 快速排序第一种实现思路(Hoare版本)🚀

第一趟排序思路

1.选择关键值做key,这里我们选L做key,R先走,反之L先走

2.R遍历到小于key的位置后停止,L开始遍历,直到大于key的位置停止,然后两个位置的值交换

3.R和L相遇之后将key与相遇位置互换

以下是第一趟排序代码

//快速排序
void QuickSort(int* a, int left, int right)
{
    int end, begin;
    end = left, begin = right;

    int keyi = left;
    while(left < right)
    {
        while(left < right && a[right] >= a[keyi])
                --right;

        while(left < right && a[left] <= a[keyi])
                ++left;

        Swap(&a[left], &a[right]);
    }

    Swap(&a[left], &a[keyi]);
}

完成以上步骤之后:

所有大于key的值都在key的左边,小于key的值都在key的右边

然后我们将这组数据分为三个区间:

然后我们将除了key之外的两个区间进行与上面相同的步骤

使用递归的方法(如图):

如图所示:当递归结束之后,这组数据就有序了

方法一完整代码实现(Hoare版本)🚀:
//快速排序
int QuickSort1(int* a, int left, int right)
{
    int keyi = left;
    while(left < right)
    {
        while(left < right && a[right] >= a[keyi])
                --right;

        while(left < right && a[left] <= a[keyi])
                ++left;

        Swap(&a[left], &a[right]);
    }

    Swap(&a[left], &a[keyi]);
    keyi = left;

    return keyi;
}

void test(int* a, int left, int right)
{
    //递归结束的条件
    if(left > right)
    {
        return;
    }

    int keyi = QuickSort1(a, left, right);
    test(a, left, keyi - 1);
    test(a, keyi + 1, right);
}

关于关键值key,这里还有一些优化

->1.随机取key

//随机取key
int randi = left + (rand() % (right - left));
Swap(&a[randi], &a[left]);
int keyi = left;

->2.三数取中(取三个数中第二大的那个值的下标)

//三数取中
int GetMidNum(int* a, int left, int right)
{
    assert(a);
    
    int mid = left + (right - left)/2;

    if(a[mid] > a[left])
    {
        if(a[mid] < a[right])
        {
            return mid;
        }
        else if(a[left] > a[right])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
    else
    {
        if(a[mid] > a[right])
        {
            return mid;
        }
        else if(a[left] < a[right])
        {
            return left;
        }
        else
        {
            return right;
        }
    }
}
//三数取中
int midi = GetMidNum(a, left, right);
Swap(&a[midi], a[left]);
int keyi = left;

一直往下递归,每递归一次就会少一个key(少一个数),该递归形状与二叉树相似,所以高度可以看作:logn

虽然N一直在减(减的很少),但是递归到最后,其实N还是在N这个量级里面,将它看作N就可以了

所以快速排序的时间复杂度大致可以看作为:O(N*logN)

这两个优化有什么用呢?

 1.如果key固定在R或者L上取值的话,当数组有序时会变得很麻烦,甚至栈溢出

一直往下递归,每次递归只有一个key,所以当数据很大时候很容易造成栈溢出

2.我们一般使用的是三数取中,因为这个找key比较科学

->2. 第二种实现思路(挖坑法)🚀

大家看图体会一下

挖坑法比第一种方法要简介一点

我这里直接给代码了

方法二完整代码实现🚀
//快速排序挖坑法
int QuickSort2(int* a, int left, int right)
{
	//三数取中
	int midi = GetMidNum(a, left, right);
    if(midi != left)
	Swap(&a[left], &a[midi]);

	//这里必须存left这个为位置的值,来保存这个位置的值
	int key = a[left];
	int hole = left;

	while (left < right)
	{
		while (left < right && a[right] >= key)
			--right;

		a[hole] = a[right];
		hole = right;

		while (left < right && a[left] <= key)
			++left;

		a[hole] = a[left];
		hole = left;
	}
	a[hole] = key;
    
    return hole;
}

void test(int* a, int left, int right)
{
    //递归结束的条件
    if(left > right)
    {
        return;
    }

    int keyi = QuickSort2(a, left, right);
    test(a, left, keyi - 1);
    test(a, keyi + 1, right);
}
->3.第三种实现方法(前后指针法)🚀

这个方法是必须掌握的,前后指针法比上面两种代码量都要少

实现思路是:以上图为例

cur找到比key值小的后,++prev,然后prev与cur位置的值交换,cur++,当cur遍历完数组之后结束

方法三完整代码实现🚀
//前后指针法
int QuickSort3(int* a, int left, int right)
{
    //三数取中
    int midi = GetMidNum(a, left, right);
    if(midi != left)
    Swap(&a[midi], &a[left]);
    int keyi = left;

    int prev = left;
    int cur = left + 1;
    
    while(cur <= right)
    {
        if(a[cur] < key && ++prev != cur)
            Swap(&a[cur], &a[prev]);

        ++cur;
    }    
    
    Swap(&a[prev], &a[keyi]);
    keyi = prev;
}

void test(int* a, int left, int right)
{
    if(left > right)
    {
        return;
    }
    
    int keyi = QuickSort3(a, left, right);
    test(a, left, keyi - 1);
    test(a, keyi + 1, right);
}

3.1.5 优化快速排序🚀

如果后面的递归,我们自己来排,不使用递归,那快速排序是不是会快?

我们来试一下

void test(int* a, int left, int right)
{
    if(left > right)
    {
        return;
    }
    
    //小区间优化--小区间直接使用插入排序
    if((right - left + 1) > 10)
    {
        int keyi = QuickSort(a, left, right);
        test(a, left, keyi - 1);
        test(a, keyi + 1, right);
    }
    else
    {
        InsertSort(a + left, right - left + 1);
    }
}

3.1.6 快速排序的非递归现实🚀

这里我们使用深度优先遍历,利用栈来实现快速排序的非递归

这里不知道栈的,可以看我上几篇博客  跳转栈

//非递归方法,使用栈实现
void test(int* a, int left, int right)
{
    ST st;
    STInit(&st);
    STPush(&st, right);
    STPush(&st, left);

    while(!STEmpty(&st))
    {
        int begin = STTop(&st);
        STPop(&st);
        int end = STTop(&st);
        STPop(&st);
        
        int keyi = QuickSort3(a, begin, end);
        
        if(begin < keyi - 1)
        {
            STPush(&st, Keyi - 1);
            STPush(&st, begin);
        }
        if(end > keyi + 1)
        {
            StPush(&st, end);
            STPush(&st, keyi + 1);
        }
    }
}

3.1.7快速排序三路划分🚀(解决重复数据)

对于有很多数值相等的排序任务来说,将数值想相等的元素挤到中间,每一次确定位置的元素变多了递归处理的区间长度也减少了,总的来说,三路划分是应对相等元素较多的排序任务

注:lt:less than  gt:greater than

基本思想🚀:

将小于V的值放在第一个区间( i 位置的元素和 第一个区间最后一个元素的后一个位置交换),等于V的放在第二个区间(i 继续往后走,不用处理) 大于V的放在第三个区间(将 i位置上的元素与第三区间上的第一个元素的前一个交换),直至i  > gt结束,最后将key与第一个区间最后一个元素交换位置的值,使key到第二个区间。i遍历整个数组

代码实现🚀:
//快速排序三路划分
void QuickSortThreeWay(int* a, int left, int right)
{
	if (left >= right)
		return;

	int begin = left, end = right;
	//三数取中
	int midi = GetMidNum(a, left, right);
    if(midi != left)
	Swap(&a[midi], &a[left]);
	int keyi = left;

	int lt = left, gt = right;
	int i = left + 1;

    //分区间
	while (i <= gt)
	{
		if (a[i] < a[keyi] && ++lt != i)
		{
			Swap(&a[lt], &a[i]);
			++i;
		}
		else if (a[i] == a[keyi])
		{
			++i;
		}
		else if (a[i] > a[keyi])
		{
			Swap(&a[gt], &a[i]);
			--gt;
		}
		else
		{
			++i;
			continue;
		}
	}

	Swap(&a[lt], &a[keyi]);
	
    //对另外两个区间递归排序
	QuickSortThreeWay(a, begin, lt - 1);
	QuickSortThreeWay(a, gt + 1, end);
}

快速排序的特征总结:

1.快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫:快速排序

2.时间复杂度:O(N*logN)

3.空间复杂度:O(logN)

4.稳定性:不稳定

4.归并排序🚀

4.1 归并排序(递归实现)🚀

4.1.1基本思想🚀

归并排序( MERGE-SORT )是建立在归并操作上的一种有效的排序算法 , 该算法是采用分治法( Divide andConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。 归并排序核心步骤:

4.1.2 代码实现(递归实现):🚀

//归并排序
void _MergeSort(int* a, int left, int right, int* tmp)
{
	if (left >= right)
		return;

	int mid = left + (right - left) / 2;

	_MergeSort(a, left, mid, tmp);
	_MergeSort(a, mid + 1, right, tmp);

	int i = left;
	int begin1 = left, end1 = mid;
	int begin2 = mid + 1, end2 = right;

	while (begin1 <= end1 && begin2 <= end2)
	{
		if (a[begin1] <= a[begin2])
		{
			tmp[i++] = a[begin1++];
		}
		else 
		{
			tmp[i++] = a[begin2++];
		}
	}

	while (begin1 <= end1)
	{
		tmp[i++] = a[begin1++];
	}

	while (begin2 <= end2)
	{
		tmp[i++] = a[begin2++];
	}

	memcpy(a + left, tmp + left , sizeof(int) * (right - left + 1));
}

void MergeSort(int* a, int left, int right)
{
	int* tmp = (int*)malloc(sizeof(int) * (right - left + 1));
	if (NULL == tmp)
	{
		perror("MergeSort::malloc");
		return;
	}

	_MergeSort(a, left, right, tmp);

	free(tmp);
}

方便大家理解我这里画一个递归展开图:

4.2 归并排序(非递归实现)🚀

代码实现1(一次性拷贝进a数组):🚀

//归并排序(非递归)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("MergeSort::malloc");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i +=2 * gap)  //这里的i +=2 * gap是跳跃已经排完的区间
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			int j = i;
			printf("[%d][%d],[%d][%d] ", begin1,end1,begin2,end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2]) //这里加上等于号,归并排序就变的稳定了
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}
		gap *= 2;
        //一次拷贝进a数组,需要放在循环中,每次都给改变a数组的值
	    memcpy(a, tmp, sizeof(int) * n);
		printf("\n");
	}

	free(tmp);
	tmp = NULL;
}

测试运行:

测试用例:

出现这个情况不要着急,一般是细节没做好导致越界

我将它分割区间的过程打印出来了

我们只有10个元素下标0~9,但是这里我用红色圈出来的都比9要大很明显这里区间范围取大了

这里我们将复杂问题分解为简单问题:分类处理

1. 因为begin1等于i,是绝对不可能越界的,当end1越界时,这时候就不归并了

2.end1没有越界,begin2越界了,end2肯定也越界了,跟begin1一样处理,但是要将它们改为不存在区间

如图:

3.begin1,begin2没有越界,end2越界了,修正end2,继续归并

然后tmp将数据放入a数组中

以上就是一次性拷贝进a数组的坏处

修正之后代码:

//归并排序(非递归)
void MergeSortNonR(int* a, int n)
{
	int* tmp = (int*)malloc(sizeof(int) * n);
	if (NULL == tmp)
	{
		perror("MergeSort::malloc");
		return;
	}

	int gap = 1;
	while (gap < n)
	{
		for (int i = 0; i < n; i += 2 * gap)
		{
			int begin1 = i, end1 = i + gap - 1;
			int begin2 = i + gap, end2 = i + gap * 2 - 1;
			int j = i;
            
            //修正代码
			if (end1 >= n)
			{
				end1 = n - 1;
				begin2 = n;
				end2 = n - 1;
			}
			else if (begin2 >= n)
			{
				begin2 = n;
				end2 = n - 1;
			}
			else if (end2 >= n)
			{
				end2 = n - 1;
			}


			printf("[%d][%d],[%d][%d] ", begin1,end1,begin2,end2);
			while (begin1 <= end1 && begin2 <= end2)
			{
				if (a[begin1] <= a[begin2]) //这里加上等于号,归并排序就变的稳定了
				{
					tmp[j++] = a[begin1++];
				}
				else
				{
					tmp[j++] = a[begin2++];
				}
			}

			while (begin1 <= end1)
			{
				tmp[j++] = a[begin1++];
			}

			while (begin2 <= end2)
			{
				tmp[j++] = a[begin2++];
			}
		}
		gap *= 2;
		//一次拷贝进a数组
		memcpy(a, tmp, sizeof(int) * n);
		printf("\n");
	}
	free(tmp);
	tmp = NULL;
}

运行测试:

修正之前与修正之后对比:

对比可知,我们已经将越界部分都处理掉了

代码实现2(归并一部分就拷贝一部分进数组a):🚀

void MegerSortNonR(int* a, int n)
{
    int* tmp = (int*)malloc(sizeof(int) * n);
    if(NULL == tmp)
    {
        perror("MegerSortNonR::malloc");
        return;
    }

    int gap = 1;
    while(gap < n)
    {
        for(int i = 0; i < n; i += 2*gap)
        {
            int begin1 = i, end1 = i + gap - 1;
            int begin2 = i + gap, end2 = i + 2 * gap - 1;
            int j = i;
            
            //修正
            if(end1 <= n && begin2 >= n)
            {
                break;
            }
            else if(end2 >= n)
            {
                end2 = n - 1;
            }

            while(begin1 <= end1 && begin2 <= end2)
            {
                if(a[begin1] <= a[begin2])
                {
                    tmp[j++] = a[begin1++];
                }
                else
                {
                    tmp[j++] = a[begin2++];
                }
            }
            
            while(begin1 <= end1)
            {
                tmp[j++] = a[begin1++];
            }
            while(begin2 <= end2)
            {
                tmp[j++] = a[begin2++];
            }
            //a和tmp都得加i,因为a不加i的话,tmp + i就会覆盖a数组中前面的元素,这是memcpy的功能
            memcpy(a + i, tmp + i, sizeof(int) * (end2 - i + 1));
        }
        gap *= 2;
    }
    
    free(tmp);
    tmp = NULL;
}

如果是这样的话,对于区间的修正就简单多了

1. end1,begin2越界, 直接break就可以了,因为我们是归并一次就拷一次所以不用考虑是否会缺数据,或者覆盖的问题

2. end2越界,修正一下,继续归并

归并排序的特征总结:

1.归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。

2.时间复杂度:O(N*logN)

3.空间复杂度:O(N)

4.稳定性:稳定

5.非比较排序

 计数排序,基数排序,桶排序这三种排序都是非比较排序,因为后两种在实际中很少使用,也没什么价值,所以这里我们说明一下计数排序

5.1计数排序

计数排序又叫鸽巢原理,只适合范围集中,且范围不大的整形数组排序。

不适合范围分散或者非整形的排序,如:字符串,浮点数等

5.1.1基本思路

相对位置排序,只要是范围就紧凑的整数数据(负整数也算),就好用

5.1.2 计数排序代码实现:

//计数排序
void CountSort(int* a, int n)
{
	int max, min;
	max = a[0], min = a[0];
	for (int i = 0; i < n; ++i)
	{
		if (a[i] > max)
		{
			max = a[i];
		}

		if (a[i] < min)
		{
			min = a[i];
		}
	}
	int range = max - min + 1;
	int* CountA = (int*)calloc(range, sizeof(int));
	if (CountA == NULL)
	{
		perror("CountA::malloc");
		return;
	}

	for (int i = 0; i < range; ++i)
	{
		CountA[a[i] - min]++;
	}

	//排序
	int j = 0;
	for (int i = 0; i < range; ++i)
	{
		//因为是计数,所以使用循环
		while (CountA[i]--)
		{
			//下标加a数组最小值
			a[j++] = i + min;
		}
	}

	free(CountA);
	CountA = NULL;
}

计数排序的特征总结:

1.计数排序在数据范围集中时,效率很高,但是适用范围有限及场景有限

2.时间复杂度:O(MAX(N, 范围))

3.空间复杂度:O(范围)

4.稳定性:稳定

6.排序算法复杂度及稳定性分析

7.各个算法效率比较

一万数据排序

单位:毫秒

一千万数据排序,选择排序,插入排序,冒泡排序,就不用计算了,这组数据比较希尔排序,堆排序,快速排序,归并排序

以上就是全部内容了,数据结构初阶终于结束了!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2052970.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis 集群三主三从配置

1&#xff1a;安装 Redis安装Linux ubuntu_ubuntu离线安装redis7.2.5-CSDN博客 2&#xff1a;主从复制配置 参考 Redis主从同步配置-CSDN博客 3&#xff1a;哨兵配置 参考 Redis 哨兵模式配置-CSDN博客 4&#xff1a;集群配置 Redis 集群三主三从配置-CSDN博客 5&…

JavaScript初级——对象和函数

一、对象的简介 1、JS中的数据类型 —— String 字符串 —— Number 数值 —— Boolean 布尔值 —— Null 空值 —— Undefined 未定义 ——以上五种类型属于基本数据类型&#xff0c;以后我们看到的值只要不是上面这五种&#xff0c;则为对象 —— Object 对象 2…

仓颉编程语言-001-第一个入门程序helloworld

目录 一、概述二、环境要求2.1 硬件环境2.2 软件环境 三、使用cjc方式开发三、使用cjpm方式 一、概述 本文档是针对仓颉编程语言编写的第一个入门程序&#xff0c;通过两种方式&#xff0c;第一种方式是cjc&#xff0c;第二种方式是cjpm。关于cjc和cjpm的使用&#xff0c;请参…

【html+css 绚丽Loading】 - 000006 四象轮回镜

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享htmlcss 绚丽Loading&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495…

履带式排爆机器人技术详解

履带式排爆机器人是现代反恐、救援及危险环境处理领域中的重要工具。它们结合了先进的机械设计、智能感知、精确控制及高效算法&#xff0c;能够在复杂、危险的环境中执行排爆、侦察、取样等多种高风险任务&#xff0c;极大地保障了人员安全。履带式排爆机器人以其卓越的地面适…

IP地址申请SSL证书实现https教程

IP地址如果想实现HTTPS访问&#xff0c;则需要部署专门针对IP地址的SSL证书。为IP地址申请SSL证书并实现HTTPS加密连接是一个涉及多个步骤的过程。 下面是一份基本的教程&#xff08;以国产服务商JoySSL为例&#xff09;&#xff1a; 1 注册账号 打开JoySSL官网&#xff0c;…

第11章 第4节 软件异常的分类及其关系(软件评测师)

V模型指出&#xff0c;&#xff08;系统测试&#xff09;对概要设计进行验证&#xff0c;&#xff08;集成测试&#xff09;对详细设计进行验证&#xff0c;&#xff08;验收测试&#xff09;当追溯到用户需求说明。 1.以下关于基于V&V原理的W模型的叙述中&#xff0c;&am…

[219] 存在重复元素 II

模拟滑动窗口 /** lc appleetcode.cn id219 langjava** [219] 存在重复元素 II*/// lc codestart class Solution {public boolean containsNearbyDuplicate(int[] nums, int k) {/*** 基本思路* 模拟 动态滑动窗口* 要求窗口左右两边的元素下标差&#xff0c;小于等于 k&…

链表基础算法题

1 移除链表元素 . - 力扣&#xff08;LeetCode&#xff09; 该题的思路是创建一个新链表&#xff0c;然后遍历原链表&#xff0c;将不是要求移除的链表元素放到新链表中&#xff0c;最后返回创建的新链表 就能达到移除链表元素的作用了。 当然这只是一种做法&#xff0c;还有…

HarmonyOS 开发

环境 下载IDE 代码 import { hilog } from kit.PerformanceAnalysisKit; import testNapi from libentry.so; import { router } from kit.ArkUI; import { common, Want } from kit.AbilityKit;Entry Component struct Index {State message: string Hello HarmonyOS!;p…

Java蛋糕店烘焙店系统小程序系统源码

解锁烘焙新纪元&#xff0c;揭秘“蛋糕店烘焙店系统”的甜蜜秘籍&#xff01; &#x1f31f;【开篇&#xff1a;烘焙业的数字化浪潮】&#x1f31f; 在这个快节奏的时代&#xff0c;传统烘焙行业也迎来了它的数字化转型时刻&#xff01;你是否梦想过拥有一家高效运转、顾客满…

了解 JavaScript 中的请求 API

你准备好提升你的网络开发技能了吗&#xff1f;&#x1f680; 在当今数据驱动的世界中&#xff0c;了解如何从 API 获取和处理数据至关重要。本指南将引导您了解在 JavaScript 中发出 HTTP 请求的最新和最有效的方法&#xff0c;确保您的 Web 应用程序保持动态和前沿。 为什么请…

排序算法【冒泡排序】

一、原理 冒泡排序的原理比较简单&#xff0c;就是将待排序区域的数值挨个向后对比&#xff0c;直到比较到已排序的边界&#xff0c;就纳入已排序区域。 二、代码如下所示&#xff1a; #include <stdio.h> #include "test.h"/* 冒泡排序 */ void bubble_sort(…

【GH】【EXCEL】P2: Read DATA SET from EXCEL into GH

文章目录 ReadRead DataExcel Data sourceGH process and components instructionRead Data Read Data LiveLive Worksheet Read Read Data Excel Data source GH process and components instruction Read Data Read data from Excel Input parameters: Worksheet (Generic …

超网和无类间路由是什么?

​一、超网概述 超网是将多个连续的网络地址组合成一个增加的网络地址的技术。常用于减少路由器的路由表大小&#xff0c;网络的可扩展性。通过合并连续的子网&#xff0c;超网可以减少路由入侵的数量&#xff0c;从而提高网络的效率。 超网的实现基于合并多个具有连续IP地址…

html 首行缩进2字符

1. html 首行缩进2字符 1.1. 场景 在Html开发中让一段文字&#xff08;富文本等&#xff09;首行缩进两个文字&#xff0c;可能在前面加上8个“ ”&#xff0c;因为过去对CSS不熟悉&#xff0c;这种方法实现虽然比较直接&#xff0c;但是文字多的时候会有很多“ ”充斥在代码中…

openGauss 6.0安装过程解除对root用户依赖之gs_preinstall

目录 1.执行前提条件 1.1设置OS参数&#xff1a; 1.2定时任务权限 1.3 修改最大文件描述符 2.切换至omm用户&#xff0c;执行preinstall 3.source环境变量 4.执行gs_install 在给客户部署业务系统时&#xff0c;由于openGauss数据库的预安装过程需要用到root用户执行&am…

SD3+ComfyUI文生图生成指南

随着人工智能技术的飞速发展&#xff0c;文生图技术已经越来越成熟。SD3&#xff08;Stable Diffusion 3 Medium&#xff09;模型以其20亿参数的庞大容量&#xff0c;提供了高质量的图像生成能力。结合ComfyUI这一灵活的节点式操作界面&#xff0c;用户可以更加高效地进行创作。…

企业电脑防泄密用什么加密软件?10款2024年企业文件加密软件推荐

在当今信息化时代&#xff0c;企业数据安全已成为重中之重。文件加密软件能够有效保护敏感信息&#xff0c;防止数据泄露和未经授权的访问。本文将为您推荐十款优秀的企业文件加密软件&#xff0c;帮助企业提高信息安全性。 1.安秉网盾加密软件 安秉网盾加密软件是一款新一代…

虚拟机Linux的坑 | VMware无法从主机向虚拟机 跨系统复制粘贴拖动 文件/文本

这个情况下&#xff0c;还是没办法跨系统拖拽文件 解决办法&#xff1a; 在终端中输入命令 sudo apt-get autoremove open-vm-tools sudo apt-get install open-vm-tools sudo apt-get install open-vm-tools-desktop过程中只要需要选择是否覆盖的地方&#xff0c;都输入&…