排序算法 (C语言)

目录

1.冒泡排序

2.选择排序

3.插入排序

4.希尔排序

5.归并排序

6.快速排序

7.堆排


         本文围绕排序算法展开,对冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序以及堆排的时间复杂度,空间复杂度,代码以及代码思路做了详细概括,文章中可能出现些许错误,望指正。

1.冒泡排序

        冒泡排序是一种简单的排序算法,其基本思想是通过重复遍历待排序的数列,比较相邻的元素,并将顺序错误的元素交换过来,从而把最大(或最小)的元素“冒泡”到数列的一端。

以下是冒泡排序的基本步骤:

  1. 从第一个元素开始,比较相邻的两个元素。
  2. 如果第一个元素大于第二个元素,则交换它们的位置。
  3. 对每一对相邻元素进行同样的操作,一直到最后一个元素。这一遍下来,最大的元素会被移动到数列的最后一个位置。
  4. 对剩下的元素重复以上步骤,直到没有需要交换的元素为止。
//冒泡排序
void bubbleSort(int* arr, int len) {
	for (int i = 0; i < len - 1; i++) {
		// 设置一个标志,判断是否需要提前结束排序
		int swapped = 0;
		for (int j = 0; j < len - 1 - i; j++) {
			if (arr[j] > arr[j + 1]) {
				// 交换相邻元素
				int temp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = temp;
				swapped = 1; // 发生了交换
			}
		}
		// 如果没有发生交换,说明数组已经有序,可以提前结束
		if (!swapped) {
			break;
		}
	}
}

时间复杂度为 O(n^2),空间复杂度为O(1),虽然冒泡排序简单易懂,但在实际中对于大规模数据的排序效率较低,通常不推荐使用。

2.选择排序

        选择排序是一种简单直观的排序算法,其基本思想是通过不断选择最小(或最大)的元素,将其放到已排序序列的末尾,从而实现排序。

选择排序的主要步骤如下:

  1. 从待排序的数组中找出最小(或最大)元素,并将其与数组的第一个元素交换。
  2. 在剩下的未排序元素中继续寻找最小(或最大)元素,并将其与第二个元素交换。
  3. 重复以上过程,直到整个数组都有序。
//选择排序
void selectionSort(int* arr, int len) {
	if (arr == NULL || len < 2) return;

	for (int i = 0; i < len - 1; i++) {
		// 假设当前元素是最小值
		int minIndex = i;
		for (int j = i + 1; j < len; j++) {
			// 找到更小的元素
			if (arr[j] < arr[minIndex]) {
				minIndex = j; // 更新最小值索引
			}
		}
		// 如果找到的最小值不是当前值,则交换
		if (minIndex != i) {
			swap(&arr[i], &arr[minIndex]);
		}
	}
}

时间复杂度为 O(n^2),空间复杂度是 O(1),其中 n 是数组的长度。虽然它的实现比较简单,但在实际应用中效率较低,因此对于大规模数据排序不太适用。

3.插入排序 

        插入排序是一种简单的排序算法,其基本思想是在一个已排序的序列中插入一个新的元素,从而得到新的有序序列。该算法通常用于小规模的数据排序。

插入排序的主要步骤如下:

  1. 从第二个元素开始,认为前面的元素是一个已排序的序列。

  2. 取出当前元素,与已排序序列中的元素进行比较,从后向前找到合适的位置插入。
  3. 将当前元素插入到正确的位置,并保持已排序序列的顺序。
  4. 重复以上步骤,直到所有元素都被插入到正确的位置。
//插入排序
void insertsort(int* arr, int len) {
	if (arr == NULL || len == 1) return;
	int j = 0;
	for (int i = 1; i < len; i++) {
		int temp = arr[i];
		for ( j = i - 1; j >= 0; j--) {
			if (arr[j] > temp) {
				arr[j + 1] = arr[j];
			}
			else {
				//arr[j + 1] = temp;
				break;
			}
		}
		arr[j + 1] = temp;
	}
}

插入排序的时间复杂度为 O(n^2),空间复杂度为O(1),但对于基本有序的数组,它的性能会比较好,接近 O(n)。越有序越快

4.希尔排序

         希尔排序是一种基于插入排序的算法,它通过将元素分组来实现更高效的排序。希尔排序的基本思想是将整个待排序的数组分成若干个子序列,对每个子序列进行插入排序,随着排序的进行,子序列的间隔逐渐减小,最后进行一次整体的插入排序。

上面的代码实现了希尔排序,具体的实现思路如下:

  1. 分组:依据给定的步长(step),将数组分成若干个子序列。多个元素之间间隔为 step
  2. 排序:对每个子序列使用插入排序。具体步骤通过外层循环遍历从 step 开始到数组的末尾。
  3. 插入:在内部循环中,将待插入的元素(temp)与当前子序列的元素比较,如果子序列中的元素大于 temp,则将其向后移动,直到找到合适的位置为止。
  4. 逐步减小步长:随着 step 的减小,算法不断对规模增大的子序列进行插入排序,直到最后 step 为 1,这时整个序列进行最后的插入排序。
//希尔排序 
void shellsort(int* arr, int len,int step) {
	if (arr == NULL || len == 1) return;
	int j = 0;
	for (int i = step; i < len; i++) {
		int temp = arr[i];
		for (j = i - step; j >= 0; j -= step) {
			if (arr[j] > temp) {
				arr[j + step] = arr[j];
			}
			else {
				//arr[j + 1] = temp;
				break;
			}
		}
		arr[j + step] = temp;
	}
}
时间复杂度大约为 O(n1.5 ) ,也可以认为在 O(n1.3 ~ n1.7 )之间。但一般情况下会优于简单插入排序。空间复杂度为 O(1),因为它是原地排序算法。

5.归并排序

        归并排序是一种有效的排序算法,采用分治法(Divide and Conquer)进行排序。其基本思想是将数组分成两个子数组,分别对这两个子数组进行排序,然后将排好序的子数组合并在一起组成最终的排序结果。

归并排序的主要步骤如下:

  1. 分解:将待排序的数组分成两半,分别对这两半进行递归调用归并排序。
  2. 解决:当数组的长度为 1 时,该数组已经是有序的,直接返回。
  3. 合并:将两个已排序的子数组合并成一个有序的数组。
//归并排序
 #if 0
vector<int> arr2;
while (r2 < len) {
	int i = 0;
	for (l1; l1 <= r1 && l2 <= r2;) {
		if (arr[l1] < arr[l2]) {
			arr2[i] = arr[l1];
			l1++;
			i++;
		}
		else {
			arr2[i] = arr[l2];
			l2++;
			i++;
		}
	}
	if (l1 <= r1) {
		for (l2; l2 <= r2; l2++) {
			arr2[i] = arr[l2];
			i++;
		}
	}
	if (l2 <= r2) {
		for (l1; l1 <= r1; l1++) {
			arr2[i] = arr[l1];
			i++;
		}
	}
	l1 = r2 + gap;
	r1 = l1 + gap;
	l2 = r1 + 1;
	r2 = l2 + gap;
	if (r2 > len) r2 = len;
}
#endif
//   gap 代表归并段存在几个数据 
//arr 原始数据     tmp 辅助函数
static void merge(int* arr, int* tmp, int len, int gap) {
	
	int l1 = 0;
	int r1 = l1 + gap-1;
	int l2 = r1 + 1;
	int r2 = l2 + gap-1;
	if (r2 >= len) r2 = len-1;

	int i = 0;
	while (l2 < len) {
		//谁小谁下来
        //    如果有两个归并段
		while (l1 <= r1 && l2 <= r2) {

			if (arr[l1] < arr[l2])
				tmp[i++] = arr[l1++];
			else
				tmp[i++] = arr[l2++];

		}
		//第二个归并完成
		while (l1 <= r1) {
			tmp[i++] = arr[l1++];
		}
		//第一个归并完成
		while (l2 <= r2) {
			tmp[i++] = arr[l2++];
		}
		l1 = r2 + 1;
		r1 = l1 + gap - 1;
		l2 = r1 + 1;
		r2 = l2 + gap - 1;
		if (r2 >= len) r2 = len - 1;
	}
    //不足两个归并段
	for (int j = l1; j < len - 1; j++) {
		tmp[i++] = arr[j];
	}
	//将tmp导入arr
	memcpy(arr, tmp, sizeof(tmp[0]) * len);
}
void mergesort(int* arr, int len) {
	int* tmp = (int*)malloc(sizeof(int) * len);
	for (int i = 1; i < len; i *= 2) {
		//遍历次数  1*2*2*2*2==n    log(2)n
		merge(arr, tmp, len, i);
	}
	free(tmp);
}

归并排序的时间复杂度为 O(n log n),无论是在最坏情况下还是平均情况下,都是如此。它的空间复杂度为 O(n) 由于需要额外的存储空间来存放合并后的数组。

6.快速排序

       快速排序是一种高效的排序算法,采用分治法(Divide and Conquer)策略。它的主要思想是通过一个“基准”元素将待排序的数组分成两个子数组,其中左侧的元素都小于等于基准元素,右侧的元素都大于基准元素,接着递归地对这两个子数组进行快速排序。

快速排序的主要步骤如下:

  1. 选择基准:从数组中选择一个基准元素(通常选择第一个元素、最后一个元素或中间的元素)。
  2. 分区:通过一趟遍历,将数组分成两部分:小于等于基准的元素和大于基准的元素。
  3. 递归排序:对左右两个子数组递归进行快速排序。
  4. 合并:递归的基础上,子数组自动合并成一个有序的数组。
//快速排序
//   待排序序列首元素,作为基准,一次分割,将左右位置元素划分,

//分割函数,返回值,基准存放下标
static int partition(int* arr, int len,int i, int j) {
	int tmp = arr[i];
	while (i < j) {
		while (i<j && arr[j] > tmp)
			j--;
		arr[i] = arr[j];
		while (i < j && arr[i] < tmp)
			i++;
		arr[j] = arr[i];
	}
	//当i==j
	arr[i] = tmp;
	return i;

	
}
void Quick(int* arr, int len, int i, int j) {
	//一次分割之后,基准存放位置
	int index = partition(arr, len, i, j);
	if (index - i > 1) {
		Quick(arr, len, i, index - 1);
	}
	if (j - index > 1) {
		Quick(arr, len, index + 1, j);
	}

}
void QuichSort(int* arr, int len) {
	Quick(arr, len, 0, len - 1);
}

快速排序的平均时间复杂度为 O(n log n),最坏情况下为 O(n^2)(例如输入数组已经是有序的情况下),但通过随机选择基准或者三数取中法可以降低出现最坏情况的概率。其空间复杂度为 O(log n),主要消耗在递归栈上。

7.堆排

       堆排序(Heap Sort)是一种基于比较的排序算法,利用堆数据结构实现排序。堆是一种特殊的完全二叉树,满足堆的性质:在最大堆中,父节点的值大于或等于其子节点的值;在最小堆中,父节点的值小于或等于其子节点的值。

堆排序的主要步骤如下:

  1. 构建初始堆:将待排序的数组构建成一个最大堆或最小堆。
  2. 交换:将堆的根节点(最大值或最小值)与最后一个元素交换,然后减少堆的大小。
  3. 调整堆:对新的堆进行调整,使其重新满足堆的性质。
  4. 重复:重复步骤 2 和步骤 3,直到堆的大小为 1。
//堆排
/*
1.将数组调整为大根堆,
	从数组倒数第一个,非叶子节点开始,将堆顶元素,从下依次调整

堆顶元素 与 数组最后一个元素交换
*/
void swap(int* a, int* b) {
	int t = *a;
	*a = *b;
	*b = t;
}
void adjust(int* arr, int begin, int end) {
	int root = begin;
	int i = root, j = 2 * i + 1, tmp = arr[root];
	while (j <= end) {
		if (arr[j] < arr[j + 1] && j + 1 <= end) {
			j = j + 1;  //j标记左右节点,较大值
		}
		if (arr[i] < tmp) break;
		arr[i] = arr[j];

		i = j;
		j = 2 * i + 1;

	}

}

void heapsort(int* arr, int len) {
	//调整大根堆
	for (int i = (len - 1 - 1) / 2; i >= 0; i--) {
		adjust(arr,i,len);
	}
	swap(&arr[0], &arr[len - 1]);
	for (int i = 1; i < len - 1; i++) {
		adjust(arr, 0, len - 1 - i);
		swap(&arr[0], &arr[len - 1 - i]);
	}
	
}

堆排序的时间复杂度为 O(n log n),并且是一个原地排序算法,空间复杂度为 O(1)。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值