【数据结构与算法分析】一文搞定插入排序、交换排序、简单选择排序、合并排序的代码实现并给出详细讲解

文章目录

  • 排序相关的基本概念
  • 排序算法及其实现
    • 插入排序
      • 直接插入排序
      • 折半插入排序
      • 希尔排序
    • 交换排序
      • 冒泡排序
      • 快速排序
    • 合并排序
    • 归并排序
    • 简单选择排序
  • 算法比较

排序相关的基本概念

  • 排序:将数组中所有元素按照某一顺序(从小到大或从大到小)重新排列的过程。
  • 排序算法的稳定性:是指排列两特定元素经过排序后其相对位置依然一样,比如两元素值相等的元素a与b,如果排列前元素a在元素b前,排列后依旧如此,则称该排列算法是稳定的;否则,就是不稳定的。
  • 内部排序:是指排序过程中,所有参与排序元素都存放在内存中的排序,如冒泡排序。
  • 外部排序:是指排序过程中,元素不停地在内存与外存之间移动的排序,如特定的归并排序。

排序算法及其实现

插入排序

直接插入排序

  直接插入排序是一种基于比较的排序算法,其主要思想是将待排序序列分为已排序和未排序两个部分,每次将未排序的元素从后往前依次插入到已排序的序列中,直到所有元素都被插入到已排序的序列中,从而得到一个有序的序列。

  其动画演示为:

  其代码实现为:

/**************************************
* 函数功能:对数组进行直接插入排序
* 函数参数:vector&data表示待排序数组
* 函数返回值:无
**************************************/
void insertSort(vector<int> &data)
{
	// 获取数组长度
	int len = data.size();
	// 插入排序核心代码
	for (int i = 1; i < len; ++i)
	{
		// 判断前一个元素是否小于当前元素
		if (data[i - 1] < data[i]) continue;
		// 保存当前元素
		int temp = data[i];
		// 哨兵元素
		int k = i - 1;
		// 判断哨兵元素是否大于当前元素
		while (k >= 0 && temp < data[k])
		{
			data[k + 1] = data[k];
			--k;
		}
		// 将当前元素插入到合适位置
		data[k + 1] = temp;
	}
}

  直接插入排序的时间复杂度为 O(n^2),空间复杂度为 O(1)。在排序过程中,如果待排序的序列已经近似有序,则直接插入排序的效率会很高,因为少需移动已排序部分的元素。但如果待排序序列的规模很大或者是随机分布的,那么直接插入排序的效果会不如其他高效率排序算法,如快速排序等。

折半插入排序

  折半插入排序(Binary Insertion Sort)是一种基于比较的排序算法,与直接插入排序类似,只是在查找元素插入位置时,采用了折半查找的方式,从而使得查找的平均时间复杂度降低到 O(log n) 级别。

  相对于插入排序而言,虽然算法实现上需要执行二分查找,但是由于它减少了比较次数,因此在时间复杂度上有所优化。但是在实际应用中,对于小规模数据,插入排序可能比折半插入排序更加高效,因为对于小规模数据,折半插入排序反而会增加了一些不必要的开销。

  其代码实现为:

/***************************************
* 函数功能:对数组进行折半插入排序
* 函数参数:
* 函数返回值:
****************************************/
void insertHalfSort(vector<int>&data)
{
	// 获取数组长度
	int n = data.size();
	// 折半插入排序
	for (int i = 1; i < n; i++)
	{
		// 临时保存数据
		int temp = data[i];
		// 折半插入的范围
		int left = 0, righ = i-1;
		// 折半比较
		while (left <= righ)
		{
			int mid = (left + righ) / 2;
			if (data[mid] > temp) righ = mid - 1;
			else  left = mid + 1;
		}
		// 移动数据
		for (int j = i-1; j >= left ; j--)
			data[j + 1] = data[j];
		data[left] = temp;
	}
}

  折半插入排序的时间复杂度为 O(n^2),但相对于直接插入排序,它的平均时间复杂度要稍微低一些,为 O(n log n)。在元素规模较大,且待排序序列相对有序时,折半插入排序的效率明显优于直接插入排序。值得注意的是,折半插入排序的空间复杂度为 O(1),是一个原地排序算法。

希尔排序

  希尔排序(Shell Sort)是一种基于插入排序的快速排序算法。由于插入排序对于在已经排序较为有序的序列插入一个新元素时易产生最糟糕的情况,因此希尔排序的主要思想是通过先将整个待排序序列分成若干个子序列(按照一定的步长进行分组),并对每个子序列进行插入排序,从而使得原来的序列大部分有序。之后再对整个待排序序列进行一次插入排序,以此达到快速排序的目的。
  希尔排序的过程如下:

  • 选择一个增量序列,确保最后一次增量为 1。
  • 按照增量序列将待排序序列分成若干子序列,对每个子序列进行直接插入排序。
  • 每次将增量减半,重复第二步操作,直到增量为 1,对整个序列进行插入排序。

  例如:当我们需要对数据9,1,2 ,5,7,4,8,6,3,5进行排序时,其排序过程可为:
【数据结构与算法分析】一文搞定插入排序、交换排序、简单选择排序、合并排序的代码实现并给出详细讲解_第1张图片
  其代码实现为:

/***************************************
* 函数功能:对数组进行希尔排序
* 函数参数:vector&data-待排序数组
* 函数返回值:无
****************************************/
void shellSort(vector<int>&data)
{
	int len = data.size();
	// 增量变化
	for (int dk = len / 2; dk >= 1; dk = dk / 2)
	{
		// 遍历数组
		for (int i = dk + 1; i < len; ++i)
		{
			if (data[i] < data[i - dk])
			{
				int j, temp = data[i];
				// 数组记录后移
				for (j = i - dk; j >= dk && temp < data[j]; j -= dk)
					data[j + dk] = data[j];
				// 插入元素
				data[j + dk] = temp;
			}
		}
	}
}

  希尔排序的时间复杂度较为复杂,取决于增量分布的具体方式,一般为O(n log n)O(n^2)

交换排序

冒泡排序

   冒泡排序是一种基本的排序算法,它的原理是比较相邻的两个元素,如果顺序不对则交换。这样一趟下来,最大的元素就排到了最后面,然后再针对剩下的元素重复此操作,直到所有元素都有序为止。其时间复杂度为O(n^2^)

  其动态图解可为:

  其代码实现为:

/***************************************
* 函数功能:对数组进行冒泡排序
* 函数参数:vector&data 待排序的数组
* 函数返回值:无
****************************************/
void bubbleSort(vector<int>&data)
{
	// 获取数组大小
	int len = data.size();
	for (int i = 0; i < len - 1; i++) // 控制排序轮数
	{
		// 定义变量用于判断排序是否完成 提高排序效率
		bool flag = true;
		for (int j = 1; j < len - 1 - i; j++) // 控制排序次数
		{
			// 两数据不符合预设的顺序 应该发生交换
			if (data[j] < data[j-1])
			{
				int temp = data[j - 1];
				data[j - 1] = data[j];
				data[j] = temp;
				// 发生交换,排序未完成
				flag = false;
			}
		}
		// 判断排序是否完成
		if (flag)
			return;
	}
}

  这里为了避免不比较的循环,上述代码进行了改进,即添加标志位flag,用于判断本趟for是否发生交换,如果没有发生交换就说明排序已经完成了。

快速排序

  快速排序(Quicksort)是一种常用的排序算法,它的性能通常比冒泡排序和选择排序要好的多。快速排序采用分治法来实现,它的基本思想是将一个大问题分成小问题,然后再通过递归的方式解决。

  其动态图解为:

  快速排序中的划分操作,即将一个数组划分成两个子数组,其中一个子数组的所有元素都小于等于另一个子数组的所有元素。具体实现为选取第一个元素作为中枢(pivot),然后使用双指针法,将小于中枢的值放在左边,大于中枢的值放在右边,最终得到中枢的位置。具体代码实现可以见下:

/***************************************
* 函数功能:对数组进行快速排序的划分
* 函数参数:vector&data 待排序的数组
* 函数返回值:无
****************************************/
static int partition(vector<int>&data, int left, int right)
{
	// 选取划分中枢
	int tempKey = data[left];
	while (left < right)
	{
		// 将数组右端小于中枢的值移动到左端
		while (data[right] >= tempKey && right > left) --right;
		data[left] = data[right];
		// 将数组左端小于中枢的值移动到右端
		while (data[left] <= tempKey && right > left) ++left;
		data[right] = data[left];
	}
	// 循环跳出的条件左值就为中枢的位置
	data[left] = tempKey;

	return left;
}

  下面这段代码实现了快速排序算法的递归过程。具体实现为:

  • 判断数组的左右下标是否合法,若left >= right,则不需要进行排序;
  • 通过调用partition函数,将数组划分成两个子数组,并将划分后的结果传递给下一次排序;
  • 对左子数组进行排序,即对[left, temp-1]这一段进行排序;
  • 接着对右子数组进行排序,即对[temp+1, right]这一段进行排序。
/***************************************
* 函数功能:对数组进行快速排序
* 函数参数:vector&data 待排序的数组
* 函数返回值:无
****************************************/
void quickSort(vector<int>&data,int left,int right)
{
	// 排序不合理
	if (left >= right) return;
	// 获取一次划分 并且将划分后最终结果保存  并传递给下一次
	int temp = partition(data,left,right);
	// 再次进行左半部分排序
	quickSort(data,left,temp - 1);
	// 再次进行右半部分排序
	quickSort(data, temp + 1, right);
}

  可以将这段代码与划分操作的实现结合起来,得到完整的快速排序算法实现。

合并排序

  合并排序,名字来源于小编。
  这段代码实现的是归并排序中的合并操作。归并排序采用分治策略,将一个数组分成两个子数组,分别对这两个子数组进行排序,然后将排序后的子数组进行合并。具体实现为:

  • 首先获取要合并的两个数组data1和data2的大小;
  • 创建一个新的vector类型的数组res,用于存储合并后的结果;
  • 使用while循环遍历两个数组,将两个数组中值更小的元素添加到result中;
  • 最后再处理未完成的数组元素;
  • 返回合并后的数组res。
/***************************************
* 函数功能:对数组进行合并排序
* 函数参数:vector&data1 归并数组1 vector&data2  归并数组2
* 函数返回值:返回归并后的数组
****************************************/
vector<int> mergeSort(vector<int>&data1, vector<int>&data2)
{
	// 获取带归并数据的大小
	int len1 = data1.size(), len2 = data2.size();
	// 返回结果
	vector<int> res;
	int i = 0, j = 0;
	// 遍历两个数组 将数值更小的值放入结果集
	while (i < len1 && j < len2)
	{
		// 数组1中数据更大
		if (data1[i] > data2[j])
		{
			res.push_back(data2[j]);
			j++;
		}
		else
		{
			res.push_back(data1[i]);
			i++;
		}
	}
	// 遍历未完成的数组
	while (i < len1)
		res.push_back(data1[i++]);
	while (j < len2)
		res.push_back(data2[j++]);

	return res;
}

归并排序

  归并排序是一种经典的排序算法,它采用分治策略。将一个数组分成两个子数组,分别对这两个子数组进行排序,然后将排序后的子数组进行合并。

  其动态图解为:

  下面这段代码实现的是归并排序中的合并操作。具体实现为:

  • 根据传入的参数low、high和mid,定义int类型的变量i、j和k;
  • 创建一个大小为high-low+1的临时数组B,用于存储归并排序的结果;
  • 将A中的元素复制到B中;
  • 接着使用while循环和if语句,将B中两个子数组的元素进行比较,并将较小的元素放到A数组中;
  • 最后再处理未完成的数组元素;
  • 释放临时数组B。

(上述的合并排序就源自此处)

  其代码实现为:

/***************************************
* 函数功能:对数组进行合并排序
* 函数参数:vector&Array归并数组   int front归并排序的左起始位置 int mid归并排序的中间位置 int end归并排序的右终止位置
* 函数返回值:无
****************************************/
void Merge(vector<int> &Array, int front, int mid, int end) {
    vector<int> LeftSubArray(Array.begin() + front, Array.begin() + mid + 1);
    vector<int> RightSubArray(Array.begin() + mid + 1, Array.begin() + end + 1);
    int idxLeft = 0, idxRight = 0;
    LeftSubArray.insert(LeftSubArray.end(), numeric_limits<int>::max());
    RightSubArray.insert(RightSubArray.end(), numeric_limits<int>::max());
    for (int i = front; i <= end; i++) {
        if (LeftSubArray[idxLeft] < RightSubArray[idxRight]) {
            Array[i] = LeftSubArray[idxLeft];
            idxLeft++;
        } else {
            Array[i] = RightSubArray[idxRight];
            idxRight++;
        }
    }
}

  下面这段代码实现的是归并排序算法的递归过程。具体实现为:

  • 判断数组的左右下标是否合法,若low >= high,则不需要进行排序;
  • 找到中间位置mid,将数组分为两个子数组;
  • 对左子数组进行排序,即对[low, mid]这一段进行排序,调用mergeSort2函数;
  • 对右子数组进行排序,即对[mid+1, high]这一段进行排序,调用mergeSort2函数;
  • 将左右两个排序好的子数组,进行合并,调用Merge函数。
/***************************************
* 函数功能:对数组进行归并排序
* 函数参数:vector&Array归并数组   int front归并排序的左起始位置  int end归并排序的右终止位置
* 函数返回值:无
****************************************/
void MergeSort(vector<int> &Array, int front, int end) {
    if (front >= end)
        return;
    int mid = (front + end) / 2;
    MergeSort(Array, front, mid);
    MergeSort(Array, mid + 1, end);
    Merge(Array, front, mid, end);
}

  可以将递归过程与合并操作的实现结合起来,得到完整的归并排序算法实现。

简单选择排序

  简单选择排序(Selection Sort)是一种简单直观的排序算法,它的基本思想是:每一轮从待排序的元素中选出最小(或最大)的一个元素,存放到序列的起始位置,直到全部待排序的元素排完为止。

  其动态图解为:

具体实现为:

  • 使用两个for循环,外层循环依次遍历待排序数组的每个元素,内层循环则从当前元素往后查找,找到后面最小的元素;
  • 记录最小元素的位置min,找到最小元素后,如果该元素不在当前位置i,则将当前元素与最小元素交换位置。

  其代码实现为:

/***************************************
* 函数功能:对数组进行简单选择排序
* 函数参数:vector&data1 排序数组
* 函数返回值:无
****************************************/
void selectSort(vector<int>& date)
{
	// 遍历选择
	for (int i = 0; i < date.size(); ++i)
	{
		// 记录最小元素的位置
		int min = i;
		// 遍历查找i后面的元素
		for (int j = i + 1; j < date.size(); ++j)
			if (date[j] < date[min])
				min = j;
		// 判断当前元素是否为最小值
		if (min != i)
		{
			// 不使用临时变量完成交换 但是注意数据是否会越界
			date[min] = date[min] + date[i];
			date[i] = date[min] - date[i];
			date[min] = date[min] - date[i];
		}
	}
}

  简单选择排序的时间复杂度为O(n^2),空间复杂度为O(1)。由于每次选择最小元素时需要进行循环比较,因此不适合处理大量数据。可以将直接选择排序和其他排序算法的实现结合起来,得到更完整的排序算法实现。

算法比较

【数据结构与算法分析】一文搞定插入排序、交换排序、简单选择排序、合并排序的代码实现并给出详细讲解_第2张图片
  至于折半插入排序,其平均时间复杂度为O(n^2^),最优时间复杂度为O(nlogn),最差时间复杂度为O(n^2^),是一种稳定的排序算法稳定。

你可能感兴趣的:(数据结构与算法,算法,排序算法,数据结构,c语言)