- 接下来是
2
,依次与4
、3
比较并交换位置,得到[2, 3, 4, 10, 12, 1, 5, 6]
。 - 然后是
10
,因为它大于它前面的所有元素,所以直接放在末尾,数组变为[2, 3, 4, 10, 12, 1, 5, 6]
。 - 对于
12
,也是直接放在末尾,数组变为[2, 3, 4, 10, 1, 12, 5, 6]
。 - 对于
1
,将其与前面的元素比较并依次交换位置,直到放到正确的位置,得到[1, 2, 3, 10, 4, 12, 5, 6]
。 - 以此类推,直到整个数组排序完成。
插入排序对于小规模或部分有序的数据表现较好,时间复杂度在最好情况下为O(n),平均和最坏情况下为O(n^2)。在实际应用中,如果知道数据大部分已经是有序的,或者数据量不大,插入排序是一个简单而有效的选择。然而,对于大规模随机数据,通常选择更高效的排序算法,如快速排序、归并排序或堆排序等。
示例代码:
#include <stdio.h>
// 插入排序函数
void insertionSort(int arr[], int n)
{
int i, key, j;
for (i = 1; i < n; i++) // 从第二个元素开始遍历
{
key = arr[i]; // 将当前元素保存为key
j = i - 1; // 前一个元素的索引
while (j >= 0 && arr[j] > key)// 如果前一个元素大于key,则将前一个元素后移一位
{
arr[j + 1] = arr[j];
j = j - 1; // 向前移动一位
}
// 找到key的插入位置,将其插入
arr[j + 1] = key;
}
}
int main()
{
int arr[] = {4, 3, 2, 10, 12, 1, 5, 6}; // 待排序数组
int n = sizeof(arr) / sizeof(arr[0]); // 数组长度
// 打印原始数组
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
// 调用插入排序函数
insertionSort(arr, n);
// 打印排序后的数组
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
代码中,insertionSort
函数负责执行插入排序算法。对于arr
数组的每一个元素,我们从第二个元素开始(索引为1),将其作为key
,然后和它之前的元素逐一比较。如果前面的元素比key
大,我们就把前面的元素后移一位,直到找到key
的正确位置并插入。main
函数中,我们定义了待排序的数组arr
,并计算了数组的长度n
。然后,我们打印出原始数组的元素,调用insertionSort
函数进行排序,最后打印出排序后的数组元素。
选择排序
选择排序(Selection Sort)是一种简单直观的排序算法。它的工作原理是首先在未排序序列中找到最小(或最大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(或最大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
以下是选择排序的详细思路:
- 初始化:设置两个指针,一个指向已排序部分的末尾(通常初始化为数组的第一个元素的前一个位置),另一个指向未排序部分的开始(通常是数组的第一个元素)。
- 查找最小值:从未排序部分的第一个元素开始,遍历到未排序部分的末尾,寻找最小的元素。
- 交换:找到最小元素后,将其与未排序部分的第一个元素交换位置。这样,最小元素就被放到了已排序部分的末尾。
- 移动指针:将已排序部分的末尾指针向后移动一位,这样下一个位置就用来存放下一个最小元素。
- 重复过程:重复第2步到第4步,直到已排序部分的末尾指针到达数组的末尾,此时数组就已经完全排序好了。
需要注意的是,选择排序是不稳定的排序算法,即相等的元素在排序后可能会改变原有的相对顺序。
举个例子,假设有一个数组 [64, 25, 12, 22, 11]
,按照选择排序的思路,排序过程如下:
- 初始状态:
[64, 25, 12, 22, 11]
- 第一轮:找到最小元素11,与第一个元素64交换,得到
[11, 25, 12, 22, 64]
- 第二轮:在未排序部分
[25, 12, 22, 64]
中找到最小元素12,与第二个元素25交换,得到[11, 12, 25, 22, 64]
- 第三轮:在未排序部分
[25, 22, 64]
中找到最小元素22,与第三个元素25交换,得到[11, 12, 22, 25, 64]
- 第四轮:在未排序部分
[25, 64]
中找到最小元素25,无需交换,因为它已经在正确的位置 - 第五轮:只剩下一个元素64,无需排序
最终数组变为 [11, 12, 22, 25, 64]
,完成排序。
选择排序的时间复杂度为 O(n^2),其中 n 是待排序数组的长度。尽管它的效率不是最高的,但由于其实现简单,因此在教学和简单应用中仍然有一定的使用场景。
示例代码:
#include <stdio.h>
// 选择排序函数
void selectionSort(int arr[], int n)
{
int i, j, minIndex, temp;
// 遍历所有数组元素
for (i = 0; i < n - 1; i++)
{
// 假设当前索引位置的元素是最小的
minIndex = i;
// 在未排序部分查找最小元素
for (j = i + 1; j < n; j++)
{
// 如果发现更小的元素,更新最小元素的索引
if (arr[j] < arr[minIndex])
{
minIndex = j;
}
}
// 如果找到的最小元素不在当前位置,则交换它们
if (minIndex != i)
{
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
int main()
{
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
// 打印原始数组
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
// 调用选择排序函数
selectionSort(arr, n);
// 打印排序后的数组
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
快速排序
快排是一种递归思想的排序算法,先比较其他的排序算法,它需要更多内存空间,但快排的语句频度是最低的,理论上时间效率是最高的。
快速排序的基本思路是:在待排序序列中随便选取一个数据,作为所谓“支点”,然后所有其他的数据与之比较,以从小到大排序为例,那么比支点小的统统放在其左边,比支点大的统统放在其右边,全部比完之后,支点将位与两个序列的中间,这叫做一次划分(partition)。
一次划分之后,序列内部也许是无序的,但是序列与支点三者之间,形成了一种基本的有序状态,接下去使用相同的思路,递归地对左右两边的子序列进行排序,直到子序列的长度小于等于1为止。
// 黄色:支点
// 绿色:比支点小的数据
// 紫色:比支点大的数据
// 红色:用来和支点比较大小的(现在比的位置)
// 橙色:已经比较好的数据
注意:快速排序操作复杂,上述gif动图并没有完全将之展示出来
示例代码:
#include <stdio.h>
// 快速排序的函数原型声明
void quickSort(int arr[], int left, int right);
// 交换数组中两个元素的位置
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
// 快速排序函数
void quickSort(int arr[], int left, int right)
{
if (left < right)
{
// 分割数组,并返回pivot的位置
int pivotIndex = partition(arr, left, right);
// 对pivot左边的子数组进行递归排序
quickSort(arr, left, pivotIndex - 1);
// 对pivot右边的子数组进行递归排序
quickSort(arr, pivotIndex + 1, right);
}
}
// 分割数组的函数
int partition(int arr[], int left, int right)
{
// 选择最右侧的元素作为基准值
int pivot = arr[right];
int i = left - 1; // 指向小于基准值的元素的最后一个位置
for (int j = left; j < right; j++)
{
// 如果当前元素小于或等于基准值
if (arr[j] <= pivot)
{
// 将小于基准值的元素移动到左边
i++;
swap(&arr[i], &arr[j]);
}
}
// 将基准值放到正确的位置
swap(&arr[i + 1], &arr[right]);
return i + 1; // 返回基准值的索引位置
}
// 主函数
int main()
{
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
// 对数组进行快速排序
quickSort(arr, 0, n - 1);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
希尔排序
希尔排序(Shell Sort)是插入排序的一种更高效的改进版本,也称为缩小增量排序。它通过将比较的全部元素分为几个区域来提升插入排序的性能。这样可以让一个元素可以一次性地朝最终位置前进一大步。然后算法再取越来越小的步长进行排序,算法最后一步就是普通的插入排序,但是到了这步,需排序的数据几乎是已经排好了的(只剩少量的数据需要插入到已排好序的序列中),所以速度很快。
希尔排序的基本思路是:
- 选择一个增量序列:增量序列的选取对希尔排序的时间性能至关重要。通常,初始增量较大,随后增量逐渐减少,直至增量为1,即最后一次增量排序后序列基本有序。
- 分组排序:根据当前的增量值,将待排序的序列分割成若干长度为m的子序列(m为当前增量),然后对每个子序列进行直接插入排序。
- 减小增量:每次排序完成后,将增量按照一定的规则减小,通常是除以2或其他小于1的正数。
- 重复分组排序:使用新的增量值重复分组排序的步骤,直到增量减小到1。当增量为1时,整个序列已经基本有序,这时进行一次普通的插入排序,即可得到完全有序的序列。
希尔排序在开始时增量较大,这样它可以让元素移动更远,从而更快地使整个序列接近有序。随着增量的逐渐减小,排序的粒度也越来越细,直到增量为1时,完成最后的微调。
希尔排序的时间复杂度依赖于增量序列的选取,对于最优的增量序列,希尔排序的时间复杂度可以达到O(n1.3)左右,比普通的插入排序O(n2)要好很多。但是,由于希尔排序的增量序列选择是一个尚未解决的数学问题,所以希尔排序的实际性能可能会因增量序列的不同而有所差异。在实际应用中,常用的增量序列有Hibbard增量序列、Sedgewick增量序列等。
希尔排序是插入排序的一种优化,它通过将比较的全部元素分为几个区域来提升插入排序的性能,使得算法在数据量大的时候也能有较好的表现。
比如如下图所示,有无无序列:
84、83、88、87、61、50、70、60、80、99
第一遍,先取间隔为(Δ=5Δ=5),即依次对以下5组数据进行排序:
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
84、83、88、87、61、50、70、60、80、99
注意,当对84和50进行排序时,其他的元素就像不存在一样。因此,经过上述间隔为5的一遍排序后,数据如下:
50、83、88、87、61、84、70、60、80、99
50、70、88、87、61、84、83、60、80、99
50、70、60、87、61、84、83、88、80、99
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
最终的结果(50、70、60、80、61、84、83、88、87、99)是经过这一遍间隔Δ=5Δ=5的情况下达成的,接下去缩小间隔重复如上过程。例如让间距Δ=3Δ=3:
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
50、70、60、80、61、84、83、88、87、99
将上述粗体的每一组数据进行排序,得到:
50、70、60、80、61、84、83、88、87、99
50、61、60、80、70、84、83、88、87、99
50、61、60、80、70、84、83、88、87、99
50、61、60、80、70、84、83、88、87、99
最终的结果(50、61、60、80、70、84、83、88、87、99)更加接近完全有序的序列。接下去继续不断减小间隔,最终令Δ=1Δ=1,确保每一个元素都在恰当的位置。动图展示如下:
示例代码:
#include <stdio.h>
void shellSort(int arr[], int n)
{
int gap, i, j, temp;
// 初始增量设为数组长度的一半,之后每次减半
for (gap = n / 2; gap > 0; gap /= 2)
{
// 对每个子序列进行插入排序
for (i = gap; i < n; i++)
{
temp = arr[i];
// 从当前元素开始,比较前一个间隔为gap的元素
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap)
{
// 如果前一个元素比当前元素大,则交换它们
arr[j] = arr[j - gap];
}
// 找到合适的位置,插入当前元素
arr[j] = temp;
}
}
}
int main()
{
int arr[] = {84, 83, 88, 87, 61, 50, 70, 60, 80, 99};
int n = sizeof(arr) / sizeof(arr[0]);
printf("原始数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
// 调用希尔排序函数
shellSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++)
{
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
好,排序算法讲到这,五个算法,应届生起码要记住冒泡和插入。因为排序算法在面试和笔试中出现频率真的高!
查找算法基本概念
在一堆数据中,找到我们想要的那个数据,就是查找,也称为搜索,很容易想到,查找算法的优劣,取决于两个因素:
- 数据本身存储的特点
- 查找算法本身的特点
比如,如果数据存储是无序的,那么当我们想要在其中找到某个节点时,一般就只能对它们逐个比对。如果数据存储是有序且存放在一片连续的内存中,那么我们可以考虑从中间开始找(二分法)。因此可以看到,在实际应用中如果需要优化数据的查找(搜索)性能,我们主要从以上两方面入手,当然,有时数据的存储特性是无法更改的,那么此时就只能靠优化算法本身去达到提供程序性能的目的了。
查找算法在面试中出现频率低,但你要是学会一两种查找算法写在简历上,不是很加分吗?
顺序查找算法(Sequential Search Algorithm)
顺序查找算法是一种基本的查找算法,它的基本思路是从列表的一端开始,逐个检查每个元素,直到找到所需的元素或检查完所有元素为止。以下是顺序查找算法的详细思路:
- 确定查找范围:首先,需要确定在哪个数据集合或列表中进行查找。这个列表可以是一个数组、链表或其他数据结构。
- 初始化指针或索引:在列表的开始位置(通常是第一个元素)设置一个指针或索引。这个指针或索引用于跟踪当前正在检查的元素。
- 比较元素:将指针或索引指向的元素与目标值进行比较。如果两者相等,那么查找成功,返回该元素的位置或值。
- 移动指针或索引:如果当前元素不等于目标值,则将指针或索引向前移动一位,指向下一个元素。
- 重复比较和移动:重复步骤3和4,直到找到目标元素或指针或索引已经移动到列表的末尾。
- 判断查找结果:如果遍历完整个列表仍未找到目标元素,那么查找失败,返回相应的结果(如特殊值或错误信息)。
顺序查找算法的时间复杂度是O(n),其中n是列表的长度。这是因为在最坏的情况下,可能需要检查列表中的每个元素。尽管顺序查找在某些情况下可能不是最高效的算法(特别是当数据量很大或数据有序时),但它实现简单,对于小规模数据或特定场景仍然是一个可行的选择。
需要注意的是,顺序查找算法对于无序列表是有效的,但如果列表已经排序,那么可以考虑使用更高效的查找算法,如二分查找。此外,对于某些数据结构(如哈希表),可以直接通过键值快速定位到元素,无需遍历整个列表。
示例代码:
#include <stdio.h>
// 定义顺序查找函数
int sequentialSearch(int arr[], int n, int target)
{
int i;
// 遍历数组
for (i = 0; i < n; i++)
{
// 如果找到目标值,返回其索引
if (arr[i] == target)
{
return i;
}
}
// 如果没有找到目标值,返回-1
return -1;
}
int main() {
// 定义数组
int arr[] = {10, 14, 19, 26, 27, 31, 33, 35, 42, 44};
int n = sizeof(arr) / sizeof(arr[0]); // 计算数组长度
int target; // 目标值
int index; // 目标值在数组中的索引
// 获取用户输入的目标值
printf("请输入要查找的目标值: ");
scanf("%d", &target);
// 调用顺序查找函数
index = sequentialSearch(arr, n, target);
// 输出查找结果
if (index != -1)
{
printf("目标值 %d 在数组中的索引为 %d\n", target, index);
}
else
{
printf("目标值 %d 不在数组中\n", target);
}
return 0;
}
二分查找算法(Binary Search Algorithm)
二分查找算法是一种在有序数组中查找特定元素的搜索算法。其核心思想是将数组分成两半,通过比较目标值与中间元素的值,决定在哪一半中继续查找,从而每次都将查找范围减半,直至找到目标值或确定目标值不存在。以下是二分查找算法的详细思路:
- 初始化:
- 确定要查找的有序数组
arr
和目标值target
。 - 设置两个指针
left
和right
,分别指向数组的起始位置和结束位置。
- 确定要查找的有序数组
- 循环查找:
- 当
left
小于等于right
时,执行以下步骤:- 计算中间位置
mid
,可以是(left + right) / 2
(整数除法)或者(left + right) >> 1
(位运算,更高效)。
- 计算中间位置
- 当
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!