编程学习算法入门之:排序算法

为什么要学习排序算法 为什么要学习排序算法? 根据统计,早起大型机CPU资源的四分之一都花在了数据排序上面。排序算法作为基础的算法,各种操作系统、编程语言都提供了内置的实现。既然排序实现随处可见,我们为什么还要自己动手实现呢?虽然经典算法要动手写写加深印象的道理都懂,但直到近才发现,每种排序算法都“暗藏玄机”。排序算法看似简单,其实不同的算法中蕴涵着经典的算法策略。通过熟练掌握排序算法,就可以掌握基本的算法设计思想,包括暴力枚举法、时间空间置换、子问题的分治以及随机化。 1.选择排序:暴力搜索 1.1 “暴力”的定义 按 阅读详情

转自:微点阅读  https://www.weidianyuedu.com

排序是最基本的算法之一,常见的排序算法插入排序、希尔排序、选择排序、冒泡排序、堆排序、归并排序及快速排序。每个排序算法的时间复杂度是不同的,但是最优的时间复杂度是O(NlogN)。有些排序算法是原址排序(即不需要额外空间),也有一些是非原址排序,这也是需要注意的特点。同样地,还要注意排序算法是否是稳定排序,这有时候很重要。这篇文章简单地介绍各个排序算法的思想,然后使用C++实现各个排序算法。

插入排序

插入排序算法思想很简单,就是将元素插入已经有序的数组来完成排序。假定数组前i-1位置是有序的,现在你要将第i个元素插入这个有序数组,那么可以依次与第i-1,i-2...等位置的元素进行比较,直到找出一个小于该元素的位置j。将j+1i-1位置元素依次后移一个位置,然后将该元素放入第j+1位置。此时前i个位置是有序的。重复这个过程至第N个元素,就可以完成数组的排序了。

// 插入排序template <typename Comparable>void insertionSort(vector<Comparable>& v){ for (int i = 1; i < v.size(); ++i) {  // 先保存当前要插入的元素  Comparable tmp = move(v[i]);  int j = i;  for (; j > 0 && tmp < v[j - 1]; --j)  {   v[j] = move(v[j - 1]);  // 大于tmp的元素后移  }  v[j] = move(tmp);  // 插入正确的位置 }}

这里的实现我们采用泛型模板,泛型参数Comparable要求支持比较操作符<。同时我们使用了移动语义,这有利于效率的提升。后面其它算法的实现我们也都采用这种方式。从算法的实现可以看到有两层循环,那么插入排序的复杂度是O(N^2)。此外,插入排序对于两个相等的元素,并不会改变其先后顺序,所以是稳定排序。同时其不需要额外空间,也是原址排序。

希尔排序

希尔排序是以提出者(Donald Shell)命名的。希尔排序与插入排序的思想很相似,但是其使用了一个递增序列H1, H2, ..., Ht。希尔排序每个阶段处理这个递增序列中的一个元素Hk,其进行的是Hk间隔排序,就是保证对于任意的i,要有A[i] < A[i+Hk]。每个阶段的排序利用与插入排序相似的思想:处理的位置ihk, hk + 1, ..., N,而且每个位置的插入比较位置是i, i - Hk, , i - 2Hk...。希尔排序的一个特点是,如果先进行Hk间隔排序,那么Hk-1间隔排序后,Hk间隔仍然保持有序。这是希尔排序有效的重要保证。对于递增序列,常使用的是Ht = floor(N/2),而且Hk = floor((Hk+1)/2)

// 希尔排序template <typename Comparable>void shellSort(vector<Comparable>& v){ // 依次处理递增序列各个间隔值(从后往前) for (int gap = v.size() / 2; gap > 0; gap /= 2) {  // 对于每个间隔,进行插入排序  for (int i = gap; i < v.size(); ++i)  {   Comparable tmp = move(v[i]);   int j = i;   for (; j >= gap && tmp < v[j - gap]; j -= gap)   {    v[j] = move(v[j - gap]);   }   v[j] = move(tmp);  } }}

希尔排序的复杂度并不是那么直接,其与选用的递增序列有关,最差状态下为O(N^2),但是如果选用合适的递增序列,其复杂度可以达到次二次时间,如O(N^(3/2))。此外,也可以看到希尔排序是原址排序与稳定排序的。

选择排序

选择排序应该是最直观的排序算法,其将数组分为排序部分与未排序部分,每次在未排序部分选出一个最小值,然后放到排序部分的后面。假定数组前i-1个位置已经有序,那么从未排序序列i, i+1,..., N中找到最小值位置j,然后交换位置j与位置i上元素。选择排序也需要重复这样的过程N-1次。

// 选择排序template <typename Comparable>void selectSort(vector<Comparable>& v){ for (int i = 0; i < v.size() - 1; ++i) {  int minIdx = i;  // 寻找未排序部分的最小值位置  for (int j = i + 1; j < v.size(); ++j)  {   if (v[minIdx] > v[j])   {    minIdx = j;   }  }  swap(v[minIdx], v[i]);  // 通过交换将最小值在正确位置 }}

选择排序的时间复杂度为O(N^2),且是原址排序。但是选择排序由于交换,导致其是不稳定排序方式。

冒泡排序

冒泡排序如其名,就是让数组元素像水中气泡一样逐渐上浮,从而达到排序的目的。其也是将数组分为排序部分与未排序部分。对于未排序部分,依次比较相邻两个元素,如果前者大于后者则交换其位置。和选择排序与插入排序一样,冒泡排序也应该需要N-1次重复操作,但是有一个更好的选择。那就是在每个阶段,记录一个flag标志,如果没有进行任何一次元素交换,说明未排序部分已经有序,后面就不需要再继续冒泡了。

// 冒泡排序template <typename Comparable>void bubbleSort(vector<Comparable>& v){ bool flag = true;  // 是否交换过元素 for (int i = 0; flag; ++i) {  flag = false;   // 初始为false  // 向下冒泡  for (int j = v.size() - 1; j > i; --j)  {   if (v[j] < v[j - 1])  // 不是<=,那样是不稳定排序   {    swap(v[j], v[j - 1]);    flag = true;   }  } }}

冒泡排序时间复杂度也是O(N^2),而且是原址排序。冒泡排序也是稳定排序,但是要注意交换条件。

归并排序

前面所讨论的排序算法都是复杂度为O(N^2)的低效率排序算法。下面的算法都是时间复杂度为O(NlogN)的高级算法。我们从归并算法说起,归并算法是基于分治策略。归并算法的基础是合并两个已经有序的子数组,将两个已经有序的子数组进行合并是容易的。比如两个有序子数组AB,然后有一个输出数组C。此时你需要三个位置索引ijk,每次比较A[i]B[j],然后将最小者复制到C[k],同时递增相应的位置索引。重复上述过程知道某一个子数组遍历完,未遍历完的子数组剩余部分直接复制到输出数组就完成整个合并过程。利用合并,归并排序算法的步骤为:(1)将数组分为两个大小相等的子数组;(2)对每个子数组进行排序,除非子数组比较小,否则利用递归方式完成排序;(3)合并两个有序的子数组,完成排序。

// 归并排序的辅助方法:合并两个有序数组// v为要排序数组,tmp为辅助数组// left为左子数组的开始位置,right为右子数组的开始位置,end为结束位置template <typename Comparable>void merge(vector<Comparable>& v, vector<Comparable>& tmp, int left, int right, int end){ int tmpPos = left; int leftEnd = right - 1; int num = end - left + 1;  // 总数 // 合并两个子数组直到某一个子数组遍历完 while (left <= leftEnd && right <= end) {  if (v[left] <= v[right])  {   tmp[tmpPos++] = move(v[left++]);  }  else  {   tmp[tmpPos++] = move(v[right++]);  } } // 处理左子数组剩余部分 while (left <= leftEnd) { tmp[tmpPos++] = move(v[left++]); } // 处理右子数组剩余部分 while (right <= end) { tmp[tmpPos++] = move(v[right++]); } // 合并结果复制到原数组 while (num > 0) {  v[end] = move(tmp[end]);  --end;  --num; }}// 归并合并的内部调用函数// v为要排序数组,tmp为辅助数组// left要排序数组部分的开始位置,right是结束位置template <typename Comparable>void mergeSort(vector<Comparable>& v, vector<Comparable>& tmp,  int left, int right){ if (left < right) {  int mid = (left + right) / 2;  // 递归处理每个子数组  mergeSort(v, tmp, left, mid);  mergeSort(v, tmp, mid + 1, right);  // 合并  merge(v, tmp, left, mid + 1, right); }}// 归并排序template<typename Comparable>void mergeSort(vector<Comparable>& v){ vector<Comparable> tmp(v.size()); mergeSort(v, tmp, 0, v.size() - 1);}

归并排序的时间复杂度是O(NlogN),但是其唯一的问题是需要一个额外的数组空间,所以是非原址排序。但是其也是稳定排序。

堆排序

堆排序是利用堆来进行排序。堆一种特殊的二叉树,对于最大堆来说,其每个节点的值都大于或者等于其子节点的值。可以使用数组来存储堆:将根节点放在第一个数组位置,根节点的左右子节点分别存储在数组的第二与第三位置,然后其左子节点的左右子节点放置在第四与第五位置,依次类推。如果数组索引是从0开始的,对于位置i处的节点,其左子节点位置为2*i+1,其右子节点位置为2*(i+1)。要进行堆排序,首先要建立堆。要构建堆,需要使用一个”下沉过程“,这里我们称为siftdown:首先将位于根节点的键值与其子节点的较大键值进行比较,如果根节点的键值较小,那么就交换根节点与子节点,然后对该子节点重复这个过程,直到到达叶节点或者根节点的键值不小于其子节点的键值。假定一个堆的深度为d,那么首先可以将深度为d-1的节点使用siftdown过程,这样深度为d-1的子树满足堆性质,然后对深度为d-2的节点使用siftdown过程,······,最后处理堆的根节点,此时这个树满足堆性质。一旦我们构建好了堆,我们可以在保持堆性质的同时重复删除根节点,得到的这些根节点是有序的,从而达到排序的目的。怎么在删除根节点之后还保持堆性质呢?这里有一个技巧,我们可以交换根节点与最右子节点的键值,此时将堆将缩减一个元素(最右子节点),然后对当前根节点调用siftdown。我们知道最右子节点恰好是数组最后的位置,所以重复这一过程,可以达到原址排序的目的。

// 返回节点i的左子节点位置inline int leftChild(int i) { return 2 * i + 1; }// 堆排序辅助函数// v是存储堆的数组,i是要下沉的节点,n代表当前堆的大小template <typename Comparable>void siftDown(vector<Comparable>& v, int i, int n){ int child; Comparable tmp = move(v[i]);  // 记录要下沉的值 while (leftChild(i) < n) {  child = leftChild(i); // 左子节点  // 寻找最大子节点  if (child != n - 1 && v[child] < v[child + 1])  {   ++child;  }  if (tmp < v[child])  // 子节点上移  {   v[i] = move(v[child]);   i = child;  }  else  // 终止  {   break;  } } v[i] = move(tmp);  // 下沉到正确位置}template <typename Comparable>void heapSort(vector<Comparable>& v){ // 先建立堆 for (int i = v.size() / 2 - 1; i >= 0; --i) {  siftDown(v, i, v.size()); } // 重复删除根节点 for (int i = v.size() - 1; i > 0; --i) {  swap(v[i], v[0]);  // 交换根节点与最右子节点  siftDown(v, 0, i);  // 下沉根节点 }}

堆排序的时间复杂度也是O(NlogN),其不需要额外空间,是原址排序。但是由于进行了根节点与最右子节点的交换,堆排序是不稳定的。

快速排序

快速排序与归并排序有相似之处,其采用的也是分治的策略。快速排序也将数组划分为两部分,但是其划分是根据一个选定的中心点(pivot),前半部分是小于pivot值,后半部分大于pivot。不断重复这种策略在每个子数组上,即可完成排序。快速排序的一个关键点是选择中心点,选择中心点后要将原数组分割成两部分。如果中心点选择不恰当,那么会导致分割的两个子数组大小严重不平衡,这样快速排序的性能就会恶化。一个比较好的策略是选择数组最左边、最右边与中心位置的中间值,即Median-of-Three策略:

// 快速排序:选定中心点策略 // v是排序数组,left与right分别是要分割数组的左右边界template <typename Comparable>const Comparable& median3(vector<Comparable>& v, int left, int right){ int mid = (left + right) / 2; if (v[mid] < v[left]) { swap(v[mid], v[left]); } if (v[left] > v[right]) { swap(v[left], v[right]); } if (v[mid] > v[right]) { swap(v[mid], v[right]); } // left位置的值小于等于pivot,right位置的值一定大于等于pivot, // 要分割的数组变成left+1到right-1 swap(v[mid], v[right - 1]);  // 将pivot放到right-1位置处 return v[right - 1];}

这个选择中心点的策略很简单,但是最后把选择的中心点存储在数组倒数第二个位置,这个是为分割做准备的。一旦选定中心点,那么就要根据中心点将数组分为左右两部分。一个比较好的策略是采用左右夹逼。假定要分割的数组是A[left], A[left+1], ..., A[right]。此时记住A[right]此时存储的是中心点的值。我们设置两个位置索引变量iji从最左侧left开始,j从最右侧right-1开始。我们将i向右移动,直到此位置处的值大于或者等于pivot,同时我们将j向左移动,直到此位置处的值小于或者等于pivot。如果此时i还在j的左侧,我们交换位置i和位置j处的值。然后重复上面的过程直到i出现在j的右侧,此时我们只需要交换位置i与位置right处的值就完成了分割。完成分割后,我们只需要递归处理每个子数组即可。

// 快速排序辅助函数template <typename Comparable>void quickSort(vector<Comparable>& v, int left, int right){ if (left + 1 < right) // 3个及以上元素 {  Comparable pivot = median3(v, left, right);  // 中心点  int i = left, j = right - 1;  while (true)  {   while (v[++i] < pivot) {}  // i右移   while (v[--j] > pivot) {}  // j左移   if (i < j)   {    swap(v[i], v[j]);   }   else   {    break;   }  }  swap(v[i], v[right - 1]);  // 对子数组递归  quickSort(v, left, i - 1);  quickSort(v, i + 1, right); } else if (left < right)  // 两个元素 {  if (v[left] > v[right]) { swap(v[left], v[right]); } }}// 快速排序template <typename Comparable>void quickSort(vector<Comparable>& v){ quickSort(v, 0, v.size() - 1);}

递归时,要分两种情况,三个及以上元素时继续调用快速排序,但是两个元素时,必须要单独处理。因为选定中心点需要三个及以上元素。其实,快速排序对于小数组优势并不是很明显,当数组较小时,可以使用其它排序算法处理,比如插入排序:

// 插入排序template <typename Comparable>void insertionSort(vector<Comparable>& v, int left, int right){ for (int i = 1; i < v.size(); ++i) {  Comparable tmp = move(v[i]);  int j = i;  for (; j > 0 && tmp < v[j - 1]; --j)  {   v[j] = move(v[j - 1]);  }  v[j] = move(tmp); }}// 快速排序辅助函数const int SIZE = 5;template <typename Comparable>void quickSort(vector<Comparable>& v, int left, int right){ if (left + SIZE < right)  {  Comparable pivot = median3(v, left, right);  // 中心点  int i = left, j = right - 1;  while (true)  {   while (v[++i] < pivot) {}  // i右移   while (v[--j] > pivot) {}  // j左移   if (i < j)   {    swap(v[i], v[j]);   }   else   {    break;   }  }  swap(v[i], v[right - 1]);  // 对子数组递归  quickSort(v, left, i - 1);  quickSort(v, i + 1, right); } else  {  insertionSort(v, left, right); }}// 快速排序template <typename Comparable>void quickSort(vector<Comparable>& v){ quickSort(v, 0, v.size() - 1);}

快速排序的时间复杂度平均为O(NlogN),但是最差时也表现为O(N^2)。其次,快速排序也是原址排序,但是其是不稳定的。

大部分排序算法这里算是介绍完了,从比较上来看,这些排序算法最优性能为O(NlogN)。但是大家可以发现一个事实,这些算法都是通过比较来完成的,而且已经证明O(NlogN)是所有利用比较来进行排序的算法的一个下限。还有一点要说明,这些排序算法都有一个前提,那就是要排序的数组可以全部读进内存。但是当要排序的元素量非常大时,可能无法一下子将所有元素放进内存,此时需要外部排序算法。感兴趣可以去了解。

链表排序

前面的排序方法我们都是处理是vector,其实我们都默认要排序的是数组结构,那么元素可以随机存取(Random Access)。但是,我们知道有些结构是不支持随机存取的,比如链表结构。这里我们简单地讨论单链表结构:

// 单链表节点class ListNode{public: int val; ListNode* next; ListNode(int value, ListNode* nt = nullptr)  :val{value}, next{nt} {}};

链表结构只能前向遍历,这是很大的限制。但是,这并不代表上面的排序算法不能起作用,只不过要进行修改。我们先看一下如何使用插入排序来对一个链表排序。对于插入排序,关键的是要找到插入点位置,链表只能前向遍历,所以必须从头节点找到插入点位置,而不能采用之前的策略。实现就很简单了:

// 从链表头节点开始寻找插入点位置ListNode* findInsertPos(ListNode* head, int x){ ListNode* prev = nullptr; for (ListNode* cur = head; cur != nullptr && cur->val <= x;  prev = cur, cur = cur->next)  ; return prev;}// 使用插入排序对链表进行排序void insertionSortList(ListNode* & head){  哑巴节点 ListNode dummy{ INT_MIN }; // 不要执行dummy.next = head    // 每次处理一个节点 for (ListNode* cur = head; cur != nullptr;) {  ListNode* pos = findInsertPos(&dummy, cur->val);  // 确定插入位置  // 插入此位置  ListNode* tmp = cur->next;  cur->next = pos->next;  pos->next = cur;  cur = tmp; } head = dummy.next;}

上面的插入排序算法的时间复杂度还是O(N^2),并且不要额外空间。我们也可以使用归并排序对链表排序,此时的复杂度为O(NlogN)。具体实现如下:

// 归并两个有序链表ListNode* mergeList(ListNode* l1, ListNode* l2){ // 哑巴节点 ListNode dummy{ 0 }; ListNode* cur = &dummy; // 处理公共部分 for (; l1 != nullptr && l2 != nullptr;  cur = cur->next) {  if (l1->val <= l2->val)  {   cur->next = l1;   l1 = l1->next;  }  else  {   cur->next = l2;   l2 = l2->next;  } } // 处理l1剩余部分 while (l1 != nullptr) { cur->next = l1; l1 = l1->next; cur = cur->next; } // 处理l2剩余部分 while (l2 != nullptr) { cur->next = l2; l2 = l2->next; cur = cur->next; } return dummy.next;}// 归并排序链表void mergeSortList(ListNode* & head){ // 无元素或者只有一个元素 if (head == nullptr || head->next == nullptr) return; // 利用快慢指针找到中间节点 ListNode* slow = head, *fast = head; while (fast->next != nullptr && fast->next->next != nullptr) {  fast = fast->next->next;  slow = slow->next; } // 根据中间节点分割链表 fast = slow; slow = slow->next; fast->next = nullptr; mergeSortList(head);   // 处理前半部分 mergeSortList(slow);   // 处理后半部分 head = mergeList(head, slow);  // 归并}

可以看到,链表的归并排序不需要额外存储空间,这与数组的归并排序不同。

前面说过,仅通过比较的方式来进行排序的算法,其效率不可能优于O(NlogN)。但是也是存在可以在线性时间内完成排序的算法,如桶排序与基数排序。

算法排序算法:从基础到实战 大家好!我写这篇文章的初衷是想和大家一起学习进步。在计算机科学领域,算法是核心,排序算法更是基础中的基础,掌握它对提升编程能力至关重要。今天,咱们就来深入学习排序算法,希望能和大家共同成长,一起探索算法世界的奥秘。 阅读详情

相关推荐

知识图谱-构建:知识图谱构建流程【本体构建、知识抽取(实体抽取、 关系抽取、属性抽取)、知识表示、知识融合、知识存储】

旅游领域的类是用来描述旅游的抽象化概念术语,是对众多旅游个体共性的概念化描述。术语是划分类的基础,根据构建领域本体的目标和具体需要,可以确定哪些术语能作为领域本体的类。在以上抽取的众多旅游领域术语中,有些是能直接作为类的,如旅游、人物、交通方式、景区等等,而有些则不能作为类,如票价、景区门票、联系方式等等,它们只是属性。“旅游”很明显是顶层的类,是父类。然后,要确定旅游的子类,根据大众标签体现的共性和《中国分类主题词表》确定了旅游领域的11个核心概念,即本体层次结构图数据属性(固有属性)...

u013250861的博客 4万+

【C++】从0到1入门C++编程学习笔记 - 提高编程篇:STL常用算法排序算法

sort属于开发中常用算法之一,需熟练掌握。

知不足而奋进,望远山而前行 7114

【转录调控网络】代谢组学与其他组学的联合分析经典模式简介

多组学联合分析经典模式转录组学&代谢组学联合分析 转录组学&代谢组学联合分析 转录组是获得生物体内基因表达的重要方法,代谢组是生物体表型的基础和直接体现者。 转录组测序可以得到大量差异表达基因和调控代谢通路,但由于基因与表型之间很难之间关联,导致关键的信号通路难以确定,因此往往达不到预期的研究目的。代谢产物是生物体在内外调控下基因转录的终结果,是生物体表型的物质基础。 在系统生物学研究时代,生物过程复杂多变,基因调控网络复杂。针对特定的生理、病理等表型进行研究,利用转录组的数据获得大量差异

小哲的博客 6149

总结指南:从入门到精通的排序算法学习路径与资源推荐

排序算法编程的基石,本路径从简单实现到高级优化,循序渐进。关键是多实践:通过编写代码、解决LeetCode题目,并分析性能(如使用大O符号)。平均每天投入1-2小时,3-6个月可达到精通水平。记住,理论结合实践是核心——动手实现算法比单纯阅读更有效。开始你的学习之旅吧,遇到问题随时回顾资源!

2501_94074668的博客 1725

Scratch排序算法资源包:开启算法学习的新篇章

Scratch排序算法资源包:开启算法学习的新篇章 去发现同类优质开源项目:https://gitcode.com/ Scratch排序算法资源包介绍 本项目为初学者提供了两个经典排序算法——冒泡排序和选择排序的Scratch实现。它不仅易于上手,还包含详尽的教程,是学习数据结构和排序算法的理想起点。 项目介绍 在编程算法学习的领域中,排序算法始终是一个重要话题。Scratch排序算法资源包正是...

gitblog_06793的博客 883

探索排序算法的智能化未来:编程小白也能轻松掌握

新接入DeepSeek-V3模型,点击下载新版本InsCode AI IDE 探索排序算法的智能化未来:编程小白也能轻松掌握 在当今快速发展的科技时代,编程已成为一项不可或缺的技能。无论是数据科学家、软件工程师还是普通的学生和爱好者,编写高效的代码都是他们日常工作的核心。而排序算法作为计算机科学中基础且应用广泛的算法之一,一直是学习编程时绕不开的重要内容。然而,对于许多初学者来说,理解和实现...

inscode_022的博客 1408

编程入门与进阶:从网页设计到C++算法,青少年编程的完美指南【文末推荐好书】

随着科技的不断发展,编程已经成为青少年教育的重要一环。学习编程不仅能培养逻辑思维和解决问题的能力,还能为未来的职业生涯奠定坚实的基础。本文将为青少年提供一条清晰的编程学习路线,从简单的网页设计入手,逐步过渡到更为复杂的C++算法。无论你是零基础的新手,还是已经有一定编程经验的学生,这篇文章都能帮助你理清学习编程的路径,并为你提供实际的操作步骤和技巧。

一键难忘的博客 7860

shell编程排序算法_基本编程排序算法

shell编程排序算法 日历中的下一个任务,您喜欢的运动队在联盟中的排名,手机中的联系人列表,所有这些都有一个订单。 当我们处理信息时,顺序很重要。 我们使用秩序来理解我们的生活并优化我们的决策。 想象一下在字典中按字母混合顺序查找单词,或者尝试在无序的定价表中查找便宜的产品。 我们命令材料做出更合理的决定(实际上是一种幻想),这使我们对结果更有信心。 但是有一个问题:世界是混乱的,天生...

编程故事的地方 728

【数据结构】排序算法:归并排序与基数排序

本文介绍了归并排序和基数排序两种非比较型排序算法。归并排序采用分治策略,通过递归分割与合并实现排序,时间复杂度稳定为O(nlogn),空间复杂度O(n),具有稳定性。基数排序则基于数字位数进行分配收集,时间复杂度O(d(n+r)),适用于固定位数的整数或字符串排序。文章通过示例详细说明了两者的执行过程,并对比了八大排序算法的关键特性,包括时间复杂度、空间复杂度和稳定性。后通过典型例题展示了两种排序的应用场景和解题思路,强调归并排序的分治思想和基数排序的LSD实现是考试重点。(149字)

bianyuan172的博客 292

排序-希尔排序(Shell Sort)

希尔排序是插入排序的优化版本,通过设置间隔gap进行分组预排序,解决插入排序中尾部小元素需要多次移动的问题。其核心思想是:先远距离分组粗排,再逐步缩小gap至1进行终微调。算法时间复杂度约为O(n^1.3),优于普通插入排序的O(n²),且空间复杂度为O(1)。需要注意gap必须递减至1才能保证完全有序,且希尔排序是不稳定的排序方法。该算法适合处理大规模数据,通过预排序有效减少元素移动次数,显著提升排序效率。

2401_87882972的博客 484

数据结构:哈希表 算法相关 排序算法

将要存储数据的关键字和春初位置之间建立对应映射关系,存储数据时,按照映射关系寻找存储位置;查找数据,根据关键字和映射关系,寻找原数据的存储位置映射关系称为哈希函数(散列函数)目的:提高查找效率f(key)=key%10:求余法f(key)=a*key+b:一次函数法key1!=key21.开放定址法2.链地址法1.创建哈希表2.设计哈希函数3.哈希表数据插入4.哈希表的查找5.销毁哈希表6.遍历哈希表。

yxg0505的博客 577

排序-选择排序(Selection Sort)冒泡排序(Bubble Sort)

本文介绍了冒泡排序和选择排序的基本思路及时间复杂度(均为O(n^2)),并提供了代码实现。冒泡排序通过相邻元素比较交换将大值"浮"到末尾,而选择排序则每轮选择小元素置于开头。文章还给出了选择排序的双向优化版本,同时比较了七大常见排序算法的稳定性:插入、冒泡、归并排序稳定;快速、选择、希尔、堆排序不稳定。文中附有相关排序算法的扩展阅读链接。

2401_87882972的博客 238

Days 24 哈希表及排序和查找

在程序开发中,程序 = 数据结构 + 算法排序和查找是业务开发高频使用的基础算法。前面我们学习了顺序表、链表、栈队列、二叉树,本篇聚焦哈希表以及常见的排序、查找算法,梳理原理、时间复杂度、稳定性,帮大家夯实笔试面试高频考点。稳定性记忆:冒泡、插入是稳定;选择、希尔、快排不稳定。时间复杂度O (n²):冒泡、选择、插入;O (nlogn):希尔、快速排序。二分查找:只能用于有序数组,链表不适合。哈希表:核心是哈希函数 + 处理哈希冲突;冲突两大方案:拉链法、开放寻址法。

Q3365879的博客 210

【数据结构初阶】《排序(插入,选择,交换,归并,计数排序 )》---详解

三个数比一比,挑出大小排在中间的那个作为基准值(pivot)。这样做能让选出来的基准不会太极端,避免快排划分的时候一边倒。(2)用处。

2501_93351213的博客 344

排序算法实现

常见排序算法实现与比较

striving的博客 283

【LeetCode算法题精讲】排序算法精讲——从归并排序到三路快排

排序算法被称为"算法之母"。几乎每种算法教材都从排序开始,这并不是巧合——排序涵盖了分治、比较、交换、递归等核心的算法思想。面试中"手写排序"也是高频考察点,尤其是快速排序和归并排序

2401_88352165的博客 492

【数据结构】交换类排序:冒泡与快速排序

交换类排序的核心思想是:通过比较和交换相邻(或不相邻)元素的位置,逐步消除逆序对,使序列有序。打个比方:交换类排序就像整理一排乱放的书。冒泡排序是“相邻两本比较,高的往后挪”,一遍遍扫描直到全部排好。快速排序则是“先挑一本书作为标准(基准),小的放左边,大的放右边”,然后分别对左右两部分重复这个过程。算法核心策略一句话概括冒泡排序相邻元素两两比较,大的(或小的)逐步“冒泡”到两端“两两比较,大的冒泡”快速排序选一个基准,将序列分成左右两部分(左小右大),递归排序“选基准,分两边,各自排”

bianyuan172的博客 225

快速排序(递归和非递归)

本文介绍了快速排序算法的基本思想及其递归与非递归实现。基本思想是选取数组末尾元素为基准值,将小于基准值的元素置于左侧,大于基准值的置于右侧,递归处理左右子区间。非递归版本使用栈存储待处理区间下标,通过循环出栈处理区间并分割子区间,直到栈空完成排序。代码提供了完整的Java实现,包括分区函数partition、递归版本quickSortHelper和非递归版本qickSortByLoop,核心都是通过基准值划分区间并排序。非递归方法通过栈模拟递归过程。

知识备忘录 430

【数据结构】选择类排序:简单选择与堆排序

选择类排序每一轮从待排序序列中选出小(或大)的元素,放到已排序序列的末尾。打个比方:你有一堆大小不一的石头,要从小到大排列。你每次都从剩下的石头中找出小的那颗,放到已经排好的石头堆后面。重复这个过程,直到所有石头都排完。算法核心策略一句话概括简单选择排序每轮扫描剩余序列,选小元素放到前面“每轮找小的,放到前面”堆排序利用堆(完全二叉树)快速找到大/小元素“用堆结构加速选择”堆(Heap)是一种特殊的完全二叉树类型条件用途大根堆(Max Heap)任意节点的值 ≥ 其子节点的值。

bianyuan172的博客 319

Genesis2000、InCAM脚本图文教程

Genesis2000, InCAM的C-shell脚本教程,图文并茂,简单易懂,可以和作者加微信沟通

上一篇: 浅谈10个常用的JS技巧
下一篇: mysql和oracle的区别浅谈
xinyuerr
博客等级 码龄3年 2682粉丝 928原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值