数据结构排序

Posted ~千里之行,始于足下~

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构排序相关的知识,希望对你有一定的参考价值。

000000000000

文章目录


前言

> 首先,我们来介绍一下排序的常见概念

排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在内存中,根据排序过程的要求不能在内外存之间移动数据的排序


一、插入排序

1.原理

把待排序的记录按其关键码值的大小逐个插入到一个已经排好序有序序列中,直到所有的记录插入完为止,得到一个新的有序序列

2.代码实现

代码如下(示例):

void InsertSort(int * arr, int len)

  int i = 0;
  int j = 0;
  for (i = 1; i < len; i++) 
  
    int key = arr[i];
    j = i-1;
    while (j >= 0 && arr[j] > key)
    
      arr[j+1] = arr[j];
      j--;
    
    arr[j+1] = key;
  


3.时间复杂度

插入排序排第一个数最多遍历1次,第二个数最多2次… 第n个数最多n次即为逆序的情况
1 + 2 + 3 + … + n = n (1 + n) / 2
O(N2)

4. 空间复杂度

未借助辅助空间
O(1)

5.稳定性

稳定

6.应用场景

适用于元素数量比较少或元素接近有序

二、希尔排序

1.原理

先选定一个整数即gap,把待排序文件中所有记录分成个组,并对每一组内的记录进行直接插入排序.然后,缩小gap值,重复上述分组和排序的工作。当gap为1时,即为对整个序列应用直接插入排序.

2.代码实现

代码如下(示例):

void ShellSort(int * arr, int len)

  int gap = len;
  gap = gap / 3 + 1;  //取值问题?
  int k = 0;
  int i = 0;
  int j = 0;

  for (k = gap; gap > 0; gap--)
  
	  for (i = gap; i < len; i++)
	  
	    j = i - gap;
	    int key = arr[i];
	    while (j >= 0 && arr[j] > key)
	    
	      arr[j+gap] = arr[j];
	      j -= gap;
	    
	    arr[j+gap] = key;
	  
  


3.时间复杂度


gap是按照Knuth提出的方式取值的,而且Knuth进行了大量的试验统计,我们暂时就按照:O(n^1.25)到O(1.6 n^1.25)到来算。

4. 空间复杂度

未借助辅助空间
O(1)

5.稳定性

不稳定

6.应用场景

适用于任然使用插入排序,元素不是接近有序或数量大的情况下

三、选择排序

1.原理

每一次从待排序的数据元素中选出最大(或最小)的一个元素,存放在序列的最后一个位置,直到全部待排序的数据元素排完 .

2.代码实现

代码如下(示例):

代码一

void SelectSort(int * arr, int len)

  int i = 0;
  int j = 0;
  for (i = 0; i < len-1; i++)  //趟数
  
	  int maxIndex = 0;
	  for (j = 1; j < len-i; j++) //找到最大值
	  
	    if (arr[j] > arr[maxIndex])
	    
	      maxIndex = j;
	    
	  
	  if (maxIndex != len-1-i)
	  
	    swap(&arr[maxIndex], &arr[len-1-i]); //把最大元素放到末尾
	  
  



代码二是对代码一的优化, 既然每次可以找到最大值放到末尾,我们为何不每趟找到最大值放到末尾,最小值放到开头,每次处理两个数,减少趟数

代码二

void SelectSort1(int * arr, int len)


  int begin = 0;
  int end = len-1;
  while (begin < end)
  
    int maxIndex = begin; //存放最大值的下标
    int minIndex = begin; //存放最小值的下标

    int l = begin + 1;
    while (l <= end)
    
      if (arr[l] > arr[maxIndex])
      
        maxIndex = l;
      
      if (arr[l] < arr[minIndex])
      
        minIndex = l;
      
      l++;
    
    //为了解决最小值在末尾的问题
    if (end == minIndex)
      minIndex = maxIndex;
    if (maxIndex != end)//最大值与末尾元素交换
    
      swap(&arr[maxIndex], &arr[end]);
    
    if (minIndex != begin)//最小值与开头元素交换
    
      swap(&arr[minIndex], &arr[begin]);
    
    begin++,end--;
  


这条判断语句是必要的,没有会出现问题
倘若最小元素出现在末尾

找到最大的元素和最小的元素


将最大的元素与末尾元素交换,但此时最小的元素恰好在末尾,min指向的不在时最小的元素了

然后我们把min指向的元素与开头元素交换,结果最大的元素被交换了2次,
返现结尾不是最大的元素,开头不是最小的元素


3.时间复杂度

O(N2)

4. 空间复杂度

O(1)

5.稳定性

不稳定

6.应用场景

接近有序的序列(很少使用)

四、堆排序

1.原理

  1. 建堆
    升序:建大堆
    降序:建小堆
  2. 利用堆删除思想来进行排序(将堆顶元素与堆中最后一个元素交换,然后将堆顶元素向下调整,继而堆顶元素与堆中倒数第二个元素交换,继续向下调整堆顶元素…依次类推,直至堆中堆顶元素没有要交换的元素)
    建堆和堆删除中都用到了向下调整,因此掌握了向下调整,就可以完成堆排序。

2.代码实现

代码如下(示例):

//向下调整
void AdjustDown(int * arr, int len, int index)

  int father = index; //双亲节点
  int child = father*2 + 1; //孩子节点
  while (child < len)
  
    if (child + 1 < len && arr[child+1] > arr[child]) //找到左右孩子最大的
    
      child += 1;
    
    if (arr[father] < arr[child])
    
      swap(&arr[father], &arr[child]);
      father = child;
      child = child*2 + 1;
    
    else 
    
      return;
    
  


//建立大顶堆
void CreateHeap(int * arr, int len)

  int child = (len-2) / 2;//非叶子节点的最后一个双亲节点
  int i = child;
  for (; i >= 0; i--)
  
    AdjustDown(arr, len, i);
  


//堆排序
void HeapSort(int * arr, int len)

  CreateHeap(arr, len);
  int i = len-1;
  while (i > 0)
  
     swap(&arr[0], &arr[i]);
     AdjustDown(arr, i, 0);
     i--;
  


3.时间复杂度

O(NlogN)

4. 空间复杂度

O(1)

5.稳定性

不稳定

6.应用场景

Top K的问题


堆hp中建立了一个大堆(小堆),如果数组arr剩余的元素小于(大于)堆顶元素,则交换,然后堆顶元素向下调整.

五、冒泡排序

1.原理

冒泡排序每一趟都是将数字两两比较,如果不满足情况则交换,保证将最大(小)的数冒到末尾

2.代码实现

代码如下(示例):

void BubbleSort(int * arr, int len)

  int flag = 0;
  int i = 0;
  int j = 0;
  for (i; i < len-1; i++) //趟数 len-1
  
    for (; j < len-1-i; j++)
    
      if (arr[j] > arr[i+1])
      
        swap(&arr[j], &arr[j+1]); 
        flag = 1; 
      
      if (!flag)//元素有序,不需要再排序
      
        break;
      
    
  


3.时间复杂度

O(N2)

4. 空间复杂度

O(1)

5.稳定性

稳定

6.应用场景

接近有序

六、快速排序

1.原理

任取待排序元素序列中的某元素作为基准值(一般去末尾元素),按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

2.代码实现

代码如下(示例):

快排的基本框架

void QuickSort(int * arr, int left, int right)

  //递归到一定程度 元素较少时 用插入排序
  if (left >= right)
  
    return;
  
  //int mid = Partion1(arr, left, right);
  int mid = Partion1(arr, left, right);
  //左子序列排序
  QuickSort(arr, left, mid-1);
  //右子序列排序
  QuickSort(arr, mid+1, right);

基准值的选取

```c
//快排key值的选取
//三数取中法:找到最终间数据的位置
int GetMiddleIndex(int * arr, int left, int right)

  int mid = left + ((right - left) >> 1);
  if (arr[left] < arr[right])
  
    if (arr[mid] < left)
    
      return left;
    
    else if (arr[mid] > arr[right])
    
      return right;
    
    else 
      return mid; 
  
  else   //arr[left] >= arr[right] 
  
    if (arr[mid] > arr[left])
    
      return left;
    
    else if (arr[mid < arr[right]])
    
      return right;
    
    else 
      return mid; 
  


我们选取基准值为末尾元素,对左右子序列的划分

hoare版本

//hoare版本
int Partion1(int * arr, int left, int right)

  int mid = GetMiddleIndex(arr, left, right);
  swap(&arr[mid], &arr[right]);
  int tmp = right;
  int key = arr[right];
  while (left < right)
  
    //left < right 保证合法范围内
    //找到>=key的值
    while (left < right && arr[left] < key)
    
      left++;
    
    //找到<key的值
    while (left < right && arr[right] >= key)
    
      right--;
    
    swap(&arr[left], &arr[right]);
  
  if (left != tmp)
  
    swap(&arr[left], &arr[tmp]);
  
  return left;


挖坑法

//挖坑法
int Partion2(int * arr, int left, int right)

  int mid = GetMiddleIndex(arr, left, right);
  swap(&arr[mid], &arr[right]);
  int key = arr[right];//形成坑位
  while (left < right)
  
    //找到>=key的值
    while (left <= right && arr[left] < key)
    
      left++;
    
    //必须有if判断 保证left和right在同一位置 
    if (left != right)
    
       arr[right] = arr[left];//填坑后left处变成坑位
       right--;
    
    //找到<key的值
    while (left < right && arr[right] >= key)
    
      right--;
    
    if (left != right)
    
       arr[left] = arr[right];//填坑后right变成坑位
       left++;
    
  
  arr[left] = key;

  return left;


前后指针法

//前后指针法
int Partion3(int * arr, int left, int right)

  int mid = GetMiddleIndex(arr, left, right);
  swap(&arr[mid], &arr[right]);
  
  int cur = left;
  int prev = cur-1;
  int key = arr[right];
  while (cur <= right)
  
    //++prev始终指向的是>=key的第一个元素
    if (arr[cur] < key && ++prev != cur) 
    
      swap(&arr[prev], &arr[cur]);
    
    ++cur;
  
  if (++prev != right)
  
    swap(&arr[prev], &arr[right]);
  

  return prev;


快速排序的优化:

问题:

  1. 当元素个数比较多时,递归调用深度太深导致程序奔溃
  2. key值选取的不同,是快速排序效率主要因素

优化:

  1. 递归到一定程度时,left到right区间的元素比较少(达到一定阈值后),我们可以使用插入排序来减少递归调用的次数
  2. 我们可以使用上述的三数取中法来选取key值

快速排序的非递归

void QuickSortNonR(int * a, int left, int right)

  Stack st;
  StackInit(&st);
  StackPush(&st, left);
  StackPush(&st, right);

  while (!StackEmpty(&st))
  
    right =  StackTop(&st);
    StackPop(&st);
    left = StackTop(&st);
    StackPop(&st);

    if(left >= right)
    
      continue;
    

    //划分
    int div = Partion1(a, left, right);
    //[div+1, right]
    StackPush(&st, div+1);
    StackPush(&st, right);
    //[left, div]
    StackPush(&st, left);
    StackPush(&st, div);
  以上是关于数据结构排序的主要内容,如果未能解决你的问题,请参考以下文章

快速排序

快速排序5

Java实现快速排序

轻松学习快速排序(二 )-- 快速排序优化

快速排序

数据结构与算法-----快速排序