前言
算法是计算机科学的基础,它是解决计算问题的一种方法和技巧。算法可以帮助我们更高效地处理数据,并在大型数据集中找到需要的答案。在本篇博客中,我们将探讨几个常见的算法及其Java实现。
本文将详细介绍四种常见的排序算法:插入排序,选择排序,交换排序,归并排序。除了归并,每种排序又分两种排序,本篇博客,我们将学习算法的基础理论和 Java 实现方法。不仅可以帮助开发者更好地掌握计算机科学的基本知识,而且可以提高代码编写的效率和质量。跟随小编一起进入优美的 Java 排序世界吧
正文:
一、Why?
问题:为什么我们要学习算法呢?
答:主要有三个原因。
1、优化时间复杂度。一个好的算法可以大大的提高用户的体验。举个例子:比如说你在玩一个游戏,如果说一个游戏场景的响应速度需要十分钟,你的游戏体验是不是会很差呀,那如果我把它优化成一分钟,那用户的游戏体验是不是会大大提高呢?算法也是这样的,比如我们一开始就接触了冒泡排序,虽然说啊,这个算法也可以解决排序的问题,但是它的速度实在是太慢,时间复杂度实在是太高了,在数据无序的情况下是O(N2),并且对于无序的数据,这个算法还不能优化,唯一的特点就是稳定(具体什么是稳定的算法下面的内容会解释)。
2、学习算法可以提高我们的思维能力。算法的学习需要我们分析问题、设计解决方案、优化算法等等一系列思维活动,这些过程能够提高我们的抽象思维能力和逻辑思考能力。
3、算法的学习也可以帮助我们在竞争激烈的求职市场中有更多的优势。算法能力是计算机从业人员非常需要的技能之一,通过自己的算法能力的提高,可以让我们在面试中更加优秀。
二、How?
那我们该如何实现算法呢?先别急,接着往下看
2.1、算法的稳定性
在学习如何实现之前,我们该学习什么是稳定的算法,以及这个算法为什么稳定。看下图:
如上这组数据,假设这是一次考试排名的成绩排名,并且不支持并列的名次,那如上,是不是有两个第一名呢?但是又不支持并列的名次,这时我告诉你,黑色的 1 做的时间比红色的 1 少,那按照正常的逻辑,是不是应该是黑色的 1 是第一,红色的 1 是第二呢?没错,算法也是这样,如果说黑色的 1 排在了红色的 1 前面,那这就是一个稳定的算法,比如说冒泡。但是又有好兄弟会问了,那冒泡的内层循环,if
的判断加个等号不就是不稳定的了吗?所以我们还有个说法,稳定的循环可以变成不稳定的循环,但是不稳定的循环一定不能变成稳定的循环。这就是算法的稳定性判断。
2.2、插入排序
2.2.1、直接插入排序
直接插入排序?什么东西?没听过。相信大家都有这样的疑问,举个例子,斗地主,大家都玩过吧?这个就和取牌类似,比如我手上已经有两张牌了,分别是:4 8,那如果说我又拿到了一张6,是不是要和 8 进行比较呀,比完之后,发现比 8 小,再跟 8 前面的比较,发现又比 4 大,所以我把这张 6 放在了 4 和 8 的中间,手上牌的顺序就变成了:4 6 8,这就是直接插入排序。在理一遍思路:首先拿到两个数字,再拿第三个数字的时候和前面一个一个去比较,当找到比它小的,那就直接插入它后一个的位置。思路理顺,下面是代码实现:
public static void insertSort(int[] arrays) {
if (arrays.length == 0) {
System.out.println("真的一滴都没有啦!!!");
return;
}
for (int i = 0; i < arrays.length; i++) {
int tmp = arrays[i];
int j = i - 1;
for (; j >= 0; j--) {
// 找到tmp需要放的位置,把比它大的数据全往后挪;
if (arrays[j] > tmp) {
arrays[j + 1] = arrays[j];
}
// 找到位置直接退出;
else {
break;
}
}
// 因为j已经--了,所以要把原来j的位置放入tmp;
arrays[j + 1] = tmp;
}
}
写完这个代码,我们就可以来排有些数据了,运行结果如下:
怎么样,是不是就已经有序了呢?紧接着我们再来看直接插入的进阶版!!!—>希尔排序
2.2.2、希尔排序
为什么说希尔排序时进阶的直接插入呢?首先我们要知道希尔排序是怎么排的。希尔排序法的基本思想是:先选定一个整数gap,然后从数组第一个数字开始,间隔gap
取一个数组,间隔gap
取一个数字,一直取到数组结束,然后把这些数字分在同一组,再从第二个数字开始,重复上面动作,一直取到数组中所有数字都取完了才结束,然后再对每组数组进行排序,排好之后按照 gap 的间隔放好,全部组排完之后,再把gap /= 2
,重复上面操作,知道gap == 1
再排一次就结束,这时,你会发现数据已经有序了。
上面说的什么意思呢?我们来看一组数据,如下:
比如说这组数据,每一组我都用不同颜色,比较好分辨,先看第一组, 4 是不是比 9 大呢?所以这组数据就变成了这样:
然后重复这个动作,知道gap == 1
,排完如下:
这个过程就叫希尔排序了。思路理顺,下面是代码实现:
public static void shellSort(int[] array) {
if (array.length == 0) {
return;
}
int gap = array.length;
while (gap > 1) {
gap /= 2;
shell(array, gap);
}
}
private static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {// 为什么这里是+1不是+gap,在下面解释
int tmp = array[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (array[j] > tmp) {
array[j + gap] = array[j];
}
else {
break;
}
}
array[j + gap] = tmp;
}
}
首先我们来解释一下上面那个i++
为什么是+1
不是+gap
呢?按道理来说不是+gap
才是符合我们一开始所说的逻辑的吗?其实一开始说的没错,这个也没错,这个就只是没组的顺序打乱了而已,先排的第一组,然后++
了,开始排第二组,等到 + 到了gap
,就又是排第一组了。下面是运行结果:
有些人肯定会说了,你这也不能证明你这个希尔排序是对的呀,因为当gap == 1
时,不久变成直接插入排序了吗?是这样的,正确的,中肯的,一针见血的,那我们就将gap == 1
设置为调试条件,当gap == 1
时就停下来,来看看调试的结果,如下:
怎么样,这回心服口服了吧?是不是和我们分析时的顺序一样呢?
2.3、选择排序
2.3.1、选择排序
选择排序就很简单啦,就是先在数组中找到最小值,然后和第一个数字进行交换,然后再找第二小的,再和第二个数字进行交换,如此往复,就排完序了,逻辑理顺,下面是代码实现:
public void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int tmpPow = i;
for (int j = i; j < array.length; j++) {
if (array[j] < array[tmpPow]) {
tmpPow = j;
}
}
int tmp = array[i];
array[i] = array[tmpPow];
array[tmpPow] = tmp;
}
}
2.3.2、堆排序
堆排序,顾名思义,就是建立一个 Java 堆。比如说你要在一个数组中取一个升序,那你应该建立一个大顶堆还是小顶堆呢?第一个想到的肯定是小顶堆,然后每次取堆顶元素,放到数组里面就行了,但是你再仔细想一下,每次取堆顶元素,那是不是每次取完之后都要重新排堆呢?这样子的时间复杂度岂不是很高?那如果说我们反其道而行之呢?建立大顶堆呢?🆗,那我们按照这个思路继续想下去,每次取堆顶元素,和最后一个元素交换,然后就确定最大值的位置在最后了,这样子的话,那岂不是每次取完堆顶元素之后,让新的堆顶元素向下调整不就行了?那这时间复杂度大大降低了啊。好了,思路理顺,然后下面是代码实现:
public static void heapSort(int[] array) {
// 先初始化成大顶堆
createBigHeap(array);
int end = array.length - 1;
while (end > 0) {
// 先拿第一个元素和最后一个交换
swap(array, end, 0);
// 换完新的堆顶元素和倒数第二个元素调整
siftDown(array, 0, end);
// 然后再迭代往后走
end--;
}
}
private static void createBigHeap(int[] array) {
for (int i = (array.length - 1 - 1) / 2; i >= 0; i--) {
siftDown(array, i, array.length);
}
}
private static void siftDown(int[] array, int parent, int end) {
int child = (2 * parent) + 1;
while (child < end) {
if ((child + 1 < end) && (array[child] < array[child + 1])) {
child++;
}
if (array[parent] < array[child]) {
swap(array, child, parent);
}
parent = child;
child = 2 * parent + 1;
}
}
private static void swap(int[] array, int left, int right) {
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
写好了就可以去爽一下了,下面是运行结果:
2.4、交换排序
交换这类排序又分为两种:一种时冒泡排序,另外一种是快速排序,也就是我们常说的快排。
2.4.1、冒泡排序
冒泡就不用我多说了吧,老朋友了都,闭着眼睛都能敲出来,下面是代码实现:
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean flag = false;
for (int j = 0; j < array.length - 1 - i; j++) {
if (array[j] > array[j + 1]) {
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
flag = true;
}
}
if (!flag) {
break;
}
}
}
运行结果我也不演示了,接着我们来看冒泡的进阶版—>快速排序(简称快排)
2.4.2、快速排序
递归:
快排分两种,一种是递归,一种是非递归,我们先来介绍递归的方法,先要搞清楚快排是怎么排的,我们先来看个图:
上面这个是什么意思呢?意思将 5 作为基准,然后比 5 小的放前面,比5小的放后面,不过这些快排一定要从右往左开始判断,具体为什么先不用知道,等讲完快排我会详细介绍,然后第一趟走完之后就是这样的:
那你看完我的解释,走完这一趟之后,有没有想过什么时候结束一趟呢?那当然是当left >= right
的时候啦,当结束一趟之后,还要返回left
或者right
的位置(因为这时两个数是相等的,所以随便返回哪个都可以),然后继续判断这个点的左边和右边,把这个点的下标的左边和右边分别丢到挖坑法函数里面,排好递归继续走下去,我们运用这个思维,把这个图给走完
为了好分辨,红色的就是每次找的基准了,逻辑理顺之后,挖坑法的代码实现如下:
private static int partition(int[] array, int left, int right) {
int top = array[left];
while (left < right) {
while ((left < right) && (array[right] >= top)) {
right--;
}
// 走到这里说明 right 小于基准了,丢到前面去
array[left] = array[right];
while ((left < right) && (array[left] <= top)) {
left++;
}
array[right] = array[left];
}
array[left] = top;
return left;
}
那只写出一次的不行的呀,咋们还要递归往后面走,所以我们得递归往后面走,逻辑理顺,代码实现如下:
private static void interiorSort(int[] array, int start, int end) {
if (start > end) {
return;
}
// 先找到中间的下标
int midPow = partition(array, start, end);
// 再走左边和右边
interiorSort(array,start,midPow - 1);
interiorSort(array,midPow + 1, end);
}
下面是完整的代码:
public static void quickSort(int[] array) {
if (array.length == 0) {
return;
}
interiorSort(array, 0, array.length - 1);
}
private static void interiorSort(int[] array, int start, int end) {
if (start > end) {
return;
}
int midPow = partition(array, start, end);
// 再走左边和右边
interiorSort(array,start,midPow - 1);
interiorSort(array,midPow + 1, end);
}
private static int partition(int[] array, int left, int right) {
int top = array[left];
while (left < right) {
while ((left < right) && (array[right] >= top)) {
right--;
}
// 走到这里说明 right 小于基准了,丢到前面去
array[left] = array[right];
while ((left < right) && (array[left] <= top)) {
left++;
}
array[right] = array[left];
}
array[left] = top;
return left;
}
为了接口统一,quickSort
函数我也只是写了一个参数,然后具体的实现过程我在类内部实现,写完之后,就可以开始跑了,运行结果如下:
怎么样,老夫从不骗人,上面这个是递归的写法,然后下面是非递归的。
非递归:
首先我们搞清楚为什么要写非递归,你试想一下,递归是不是要开辟栈区?那如果说,我们的数据量很大的情况下,我们会栈溢出的呀,不信你看如下运行结果:
我是将数组里面的数据这样赋值的:
public static void orderArray(int[] array) {
for (int i = 0; i < array.length; i++) {
array[i] = array.length - i;
}
}
public static void main(String[] args) {
int[] array = new int[100_0000];
orderArray(array);
}
大家快看,区区一百万个数据就不行了?那这样的快排还有什么用?大家快一起嘲笑它,越想越气!
那我们该如何改进呢?首先,大家知不知道三数取中?不知道也没关系,我教大家一遍,就是拿到头,尾和中间的下标,然后比较这三个下标在数组中所对应的数值,取出比较中间的数字,避免像我上面这样极端的情况,一拿着基准就是最大的或者最小的,拿到比较中间数值,我们就可以以它来作为基准,然后让数组的数据比较均匀分布在两侧,所以我们可以写一个三数取中函数,返回中间值的下标,然后和第一个值交换,逻辑理顺,代码实现如下:
private static int threeNum(int[] array, int left, int right) {
int mid = (left + right) / 2;
if (array[left] < array[right]) {
// 大小情况: left < mid < right
if (array[left] < array[mid] && array[mid] < array[right]) {
return mid;
}
// mid < left < right
else if (array[mid] < array[left]) {
return left;
}
// left < right < mid
else {
return right;
}
}
else {
// mid < right < left
if (array[mid] < array[right]) {
return right;
}
// right < left < mid
else if (array[mid] > array[left]) {
return left;
}
// 在中间的情况
else {
return mid;
}
}
}
但是我们取出中间数字,还是要交换的,因为等会儿交换要用到的地方比较多,所以我们单独写一个函数,叫做swap
,代码如下:
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
写完这个之后,很抱歉,还是不能排序我们那个1000000
个数据,我知道你很急,但你先别急,让我先急。虽然说不能排序,但是我们还是优化了代码不是?
真正要做到不报错,不会栈溢出的,只能是非递归实现了,我不用栈了,那我不就不会栈溢出了?好办法!直接从根源上解决问题,那我们就理一下非递归实现快排需要的逻辑,首先我们需要创建一个栈(哎,此栈非彼栈,所以我们还是没使用栈),然后把每次要排序左右的下标扔到栈里面去,然后让挖坑法排序这段空间的数据,然后排完之后再出栈,排下一个,这样就解决啦。逻辑理顺,代码实现如下:
private static void interiorSort(int[] array, int start, int end) {
if (start >= end) {
return;
}
// 先三数取中
int mid = threeNum(array, start, end);
// 交换
swap(array, start, mid);
Stack<Integer> stack = new Stack<>();
stack.push(start);
stack.push(end);
// 栈不是空就可以继续
while (!stack.isEmpty()) {
// 先三数取中
mid = threeNum(array, start, end);
// 再和第一个交换,让第一个的值尽量是中间的值
swap(array, mid, start);
// 挖坑法小左大右,并返回比较中间的值的下标
int pivot = partition(array, start, end);// 返回中间的下标
// 再观察中间这个下标是不是已经到了边缘或者差一个到边缘了,因为此时
if (pivot > start + 1) {
// 再插入待排序的左边和右边
stack.push(start);
stack.push(pivot - 1);
}
if (pivot < end - 1) {
stack.push(pivot + 1);
stack.push(end);
}
// 先进去的后出来,所以要先让 end 接收
end = stack.pop();
start = stack.pop();
}
}
这样的话就能算出来了,我们可以看一下非递归的时间,代码只需这样写:
public static void testQuickSort(int[] array) {
long startTime = System.currentTimeMillis();
QuickSort.quickSort(array);
long endTime = System.currentTimeMillis();
System.out.println("时间->" + (endTime - startTime));
}
public static void orderArray(int[] array) {
for (int i = 0; i < array.length; i++) {
array[i] = array.length - i;
}
}
public static void main(String[] args) {
int[] array = new int[100_0000];
orderArray(array);
testQuickSort(array);
}
下面是运行结果:
然后这个非递归写完之后,我们还能不能再进行进一步的优化呢?当然可以,我们不是写过一个排序叫直接插入排序嘛,这个排序的特性就是当数据比较少,并且趋近于有序的情况下,这个排序是最快的,那我们就可以这样想,当数据少于一个特定的值的情况下,我们直接使用直接插入排序,我给大家计算好了,大概是小于五十的时候,所以我们就可以这样改进代码:
private static void interiorSort(int[] array, int start, int end) {
if (start >= end) {
return;
}
// 当数据少于五十个时,直接排序
if (end - start < 50) {
insertSort(array, start, end);
return;
}
// 先三数取中
int mid = threeNum(array, start, end);
// 交换
swap(array, start, mid);
Stack<Integer> stack = new Stack<>();
stack.push(start);
stack.push(end);
// 栈不是空就可以继续
while (!stack.isEmpty()) {
if (end - start < 50) {
insertSort(array, start, end);
}
else {
// 先三数取中
mid = threeNum(array, start, end);
// 再和第一个交换,让第一个的值尽量是中间的值
swap(array, mid, start);
// 挖坑法小左大右,并返回比较中间的值的下标
int pivot = partition(array, start, end);// 返回中间的下标
// 再观察中间这个下标是不是已经到了边缘或者差一个到边缘了,因为此时
if (pivot > start + 1) {
// 再插入待排序的左边和右边
stack.push(start);
stack.push(pivot - 1);
}
if (pivot < end - 1) {
stack.push(pivot + 1);
stack.push(end);
}
}
// 先进去的后出来,所以要先让 end 接收
end = stack.pop();
start = stack.pop();
}
}
// 指定范围的直接插入
private static void insertSort(int[] array, int left, int right) {
if (array.length == 0) {
return;
}
for (int i = left + 1; i <= right; i++) {
int tmp = array[i];
int j = i - 1;
for (; j >= left; j--) {
// 找到tmp需要放的位置,把比它大的数据全往后挪;
if (array[j] > tmp) {
array[j + 1] = array[j];
}
// 找到位置直接退出;
else {
break;
}
}
// 因为j已经--了,所以要把原来j的位置放入tmp;
array[j + 1] = tmp;
}
}
下面是运行结果:
虽然说这个计实只是快了一点点,但是在我们理论上还是快很多的,这只是个参考。
2.5、归并排序
归并排序和上面不太一样,归并排序只有一种,那就是归并排序。但是归并也分递归和非递归的,先讲递归的吧。
递归:
我先解释一下什么是归并排序,归并排序采用分治的思想,什么是分治呢?那就是分而治之,将一组数据先从中间截断,两边分开,然后继续截断,一直分到 1 个点为止,然后再合并成两个点,排序这两个点,然后合并四个点,排序四个点,一直合并到数组结束之后,排序也就结束了,给大家看个图就知道了。如下:
这就是整个分治过程,那要怎么表示一段一段的呢?,我们可以定义以一个s1
表示第一段的开头,再定义一个e1
表示第一段的结尾,然后定义s2,e2
定义第二段的开头和结尾,把这整个个区间的开头叫做left
,整个区间的结尾叫做right
,先将这个区间的所有数据赋值到tmpArr
数组里面,先将这个数组排好,然后再拷贝进原数组里面,逻辑理顺,关于排序的代码实现如下:
private static void merge(int[] array, int left, int mid, int right) {
int s1 = left;
int e1 = mid;
int s2 = mid + 1;
int e2 = right;
int i = 0;// tmpArr的下标
// 当s1 > e1; s2 > e2;就退出循环
int[] tmpArr = new int[right - left + 1];
while ((s1 <= e1) && (s2 <= e2)) {
// 找最小的
if (array[s1] < array[s2]) {
tmpArr[i++] = array[s1++];
}
else {
tmpArr[i++] = array[s2++];
}
}
// 出来判断一下第一段和第二段是否都走完了
while (s1 <= e1) {
tmpArr[i++] = array[s1++];
}
while (s2 <= e2) {
tmpArr[i++] = array[s2++];
}
// 然后进行copy
if (i >= 0) {
System.arraycopy(tmpArr, 0, array, left, i);
}
}
这样排序的代码就写好了,但是光有个内部排序函数可不行啊,我们还得有函数调用它,但是你想一下,我们该怎样调用呢?你看看我们上面说的这些步骤,像不像递归呀?所以我们得这样写,代码实现如下:
public static void mergeSort(int[] array) {
mergeSortFunc(array, 0, array.length - 1);
}
private static void mergeSortFunc(int[] array, int start, int end) {
if (start >= end) {
return;
}
// 找出中间的下标
int mid = (start + end) / 2;
// 先走左边和右边,走到底再开始排
mergeSortFunc(array, start, mid);
mergeSortFunc(array, mid + 1, end);
merge(array, start, mid, end);
}
为了接口统一,我多写了一个过度的方法mergeSort
,下面是运行结果:
这下子递归实现就讲完了,下面是非递归(迭代)实现。
非递归:
非递归也要先打好思路,先看下图:
看这个图,先告诉我怎么找left ; mid ; right
,这三个变量,如果实在找不到的话,看下图,我解释给你看:
先假设我们已经走到头了,从最后开始想,两个两个开始排,left ; mid ; right
三者的关系是不是如上如所示呢?这时我们可以定义一个gap
,第一次这个gep == 1
,那三者的关系不就是:
left == i; // i 表示这是第几个数字
mid == (left + gep) - 1;
right == mid + gep;
一定要记住,非递归是从最后一步开始往前走的,所以我们要一开始就要假设left + 1 == right
的情况,非递归代码具体的实现如下:
private static void mergeSortFunc(int[] array) {
int gap = 1;
while (gap < array.length) {
int i = 0;// 走一趟就要重置一次
while (i < array.length) {
int left = i;
int mid = ((left + gap) - 1);
int right = mid + gap;
// 判断是否会越界,越界了就拉回来
if (mid >= array.length) {
mid = array.length - 1;
}
if (right >= array.length) {
right = array.length - 1;
}
merge(array, left, mid, right);
// i 也要迭代往后面走
i += (gap * 2);
}
gap *= 2;
}
}
实现完成之后代码就可以跑了,和上面的结果是一样的。
三、What?
介绍完具体的实现思路以及实现过程,我们得看这些排序都有些什么特征了,具体如下:
这四种算法都是常见的排序算法,它们的具体特征如下所述:
- 插入排序(Insertion Sort)
- 算法思想:通过构建有序序列,不断将未排序的元素插入到已排序的合适位置中。
- 算法特征:稳定排序、原地排序、时间复杂度为O(N2)。
- 选择排序(Selection Sort)
- 算法思想:每次从待排序数组中选出最小的元素,放到已排序的序列的末尾。
- 算法特征:不稳定排序、原地排序、时间复杂度为O(N2)。
- 交换排序(Bubble Sort)
- 算法思想:比较相邻的元素。如果第一个比第二个大,就交换它们两个。对每一对相邻元素做同样的工作,从开始的第一对到结尾的最后一对。重复以上步骤,每次都找出未排序的元素中最大的一个,放到已排序元素的前面。
- 算法特征:稳定排序、原地排序、时间复杂度为O(N2)。
- 归并排序(Merge Sort)
- 算法思想:将待排序序列划分为若干子序列,直到每个子序列都只有一个元素,然后两两合并,直到最后只有一个有序序列。
- 算法特征:稳定排序、非原地排序、时间复杂度为O(N*log2N)。
总结
在本博客中,我们详细讲解了几种常见的算法并且使用 Java 语言进行实现。这些算法包括:
- 插入排序算法
- 选择排序算法
- 交换排序算法
- 归并排序算法
在实现这些算法的过程中,我们不仅了解了算法的原理,还学习了如何使用 Java 语言进行编程实现。通过实践,我们加深了对算法的理解和运用,并且更好地掌握了 Java 语言的编程技巧。
总的来说,学习算法不仅可以提升我们的编程能力,更重要的是可以锻炼我们的思维能力和解决问题的能力。但是要注意的是,良好的算法实现需要合理的时间和空间复杂度,因此在实现算法时,我们需要综合考虑时间和空间效率,以达到最佳的性能。
结语
在学习算法的过程中,我们可能会遇到各种各样的挑战和困难。有时候,我们可能会产生无助和挫败感,但是请不要放弃,因为成功就在不远的地方等待着我们。
对于算法的学习,我们要不断地充实自己的库存,扩大自己的知识面,不断地积累经验和技巧。同时,我们也要不断地挑战自己,不要满足于现状,永远保持学习和进步的心态。
最重要的是,要始终相信自己的潜力和能力。我们应该鼓励自己去追求梦想,勇敢地迎接挑战,相信自己,走出属于自己的一条充满挑战和机遇的路。成功虽然不是那么容易实现,但只要我们不断地努力和奋斗,我们终会成功。
在这里,我要向所有正在学习算法和编程的伙伴们送上最真挚的祝福和鼓励。让我们紧握手中的鼠标和键盘,一起奋斗,一起成长,在编程这布满荆棘的土地上走出属于自己的一条道路。
男儿不展青云志,空负平生八尺躯,一世浮生一刹那,一程山水一年华.