数据结构与算法学习笔记11:二叉树层打印/跳表/冒泡排序/选择排序/插入排序/希尔排序/计数排序

数据结构与算法学习笔记11:二叉树层打印/跳表/冒泡排序/选择排序/插入排序/希尔排序/计数排序

      • 二叉树按层打印
      • 跳跃列表(Skip List)
      • 冒泡排序(Bubble Sort)
        • 优化
      • 选择排序(Selection sort)
      • 插入排序(Insertion Sort)
      • 希尔排序/缩小增量排序(Shell Sort)
      • 计数排序(Counting Sort )

二叉树按层打印

如何将一颗二叉树按层打印,一层输出到一行上?

  • 方案一:加特殊符号
  • 方案二:双队列,dad一个队,son一个队
  • 方案三:计数
  • 方案四:记录每个结点的层数or深度
  • 方案五:把树的NULL结点都补上补成满二叉树然后一层层出(注意不要破坏树的结构,不要在原有的树里面加,而是在外面用别的容器如队列,假装补充而已)

跳跃列表(Skip List)

随机结构的有序链表

img

通过建立索引的方式,对于数据量越大的有序链表,通过建立多级索引,查找效率提升会非常明显。

一般高度都设定为 l o g 2 n log_2 n log2n

  • 特点:
  1. 每个元素插入时随机生成它的level;
  2. 最底层包含所有的元素;
  3. 每个索引节点包含两个指针,一个向下,一个向右;
  4. 跳表查询、插入、删除的时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)

二分法也是 l o g 2 n log_2 n log2n,那么为什么要用跳表呢?

  • 二分,其添加或者删除的时候会影响二分的对应关系,后面的全部要调整…插入的时间很有可能变成 O ( n ) O(n) O(n)

冒泡排序(Bubble Sort)

核心思想:比较相邻的元素。如果第一个比第二个大,就交换他们两个。

若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数C和记录移动次数M均达到最小值: C m i n = n − 1 , M m i n = 0 C_{min}=n-1,M_{min}=0 Cmin=n1,Mmin=0,所以,冒泡排序最好的时间复杂度为 O ( n ) O(n) O(n)

若初始文件是反序的,需要进行 n − 1 n-1 n1趟排序。每趟排序要进行 n − i n-i ni次关键字的比较( 1 ≤ i ≤ n − 1 1≤i≤n-1 1in1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
C m a x = n ( n − 1 ) 2 = O ( n 2 ) M m a x = 3 n ( n − 1 ) 2 = O ( n 2 ) C_{max}=\frac{n(n-1)}{2}=O(n^2)\\ M_{max}=\frac{3n(n-1)}{2}=O(n^2) Cmax=2n(n1)=O(n2)Mmax=23n(n1)=O(n2)
冒泡排序的最坏时间复杂度为 O ( N 2 ) O(N^2) O(N2),综上,因此冒泡排序总的平均时间复杂度为 O ( N 2 ) O(N^2) O(N2)

冒泡排序在排序的过程中,不需要占用很多额外的空间(就是在交换元素的时候需要临时变量存一存,这里需要的额外空间开销是常量级的),因此冒泡排序的空间复杂度为 O ( 1 ) O(1) O(1)了。

#include 

void BubbleSort(int arr[],int nLength){
    if(arr == NULL || nLength <= 0)
        return;
    int i,j,temp;
    for (i = 0; i < nLength - 1; i++) {
        for(j = 0; j < nLength -1 - i;j++){
            if (arr[j]>arr[j + 1]) {
                temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
}

void Print(int arr[],int nLength){
    if(arr == NULL || nLength <= 0)
        return;
    for (int i = 0; i < nLength; i++) {
        printf("%d ",arr[i]);
    }
    printf("\n");
}

int main(){
    int arr[] = {10,7,8,19,2,11,66,4};
    BubbleSort(arr, sizeof(arr)/sizeof(arr[0]));
    Print(arr, sizeof(arr)/sizeof(arr[0]));
    return 0;
}
优化
  • 如果后面有一部分已经有序其实不需要这么多次排序,可以用一个标记记录当前组最后一次交换的下标,以其作为下一次循环的结束边界,可以避免一些无意义的比较。如果没有发生交换,不标记,也就是说此时数组已经有序,直接跳出循环。

  • img

  • 分析:

    i的范围为0到n-2,共需要完成n-1次遍历,其关系为n-2-0+1=n-1

    而第i层排序的范围为i到n-2,共需要完成index-1次遍历,则n关系为n-2-i-1=index-1,得到i=n-index

    而j

    • 代码:
    #include 
    
    void BubbleSort(int arr[],int nLength){
        if(arr == NULL || nLength <= 0)
            return;
        int i,j,temp;
        int index;
        int count = 0;
        for (i = 0; i < nLength - 1; i++) {
            index = 0;
            for(j = 0; j < nLength -1 - i;j++){
                if (arr[j]>arr[j + 1]) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                    index = j + 1;
                }
                count++;
            }
            if (index == 0) break;
            i = nLength - index - 1;    //因为上面的for里要i++,所以先减掉一个
        }
        printf("%d\n",count);
    }
    
    void Print(int arr[],int nLength){
        if(arr == NULL || nLength <= 0)
            return;
        for (int i = 0; i < nLength; i++) {
            printf("%d ",arr[i]);
        }
        printf("\n");
    }
    
    int main(){
        int arr[] = {10,7,8,19,2,11,66,74,88};
        BubbleSort(arr, sizeof(arr)/sizeof(arr[0]));
        Print(arr, sizeof(arr)/sizeof(arr[0]));
        return 0;
    }
    
    //30次->15次
    

选择排序(Selection sort)

第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,然后再从剩余的未排序元素中寻找到最小(大)元素,然后放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。选择排序是不稳定的排序方法。

  • 平均时间复杂度: O ( N 2 ) O(N^2) O(N2)
    最佳时间复杂度: O ( N 2 ) O(N^2) O(N2)
    最差时间复杂度: O ( N 2 ) O(N^2) O(N2)
    空间复杂度: O ( 1 ) O(1) O(1)

    选择排序的交换操作介于$ 0 $和 ( n − 1 ) (n - 1) (n1)次之间。选择排序的比较操作为 n ( n − 1 ) 2 \frac{n (n - 1)}{2} 2n(n1)次之间。选择排序的赋值操作介于 0 0 0 3 ( n − 1 ) 3 (n - 1) 3(n1) 次之间。总的比较次数 N = ( n − 1 ) + ( n − 2 ) + . . . + 1 = n ( n − 1 ) 2 N=(n-1)+(n-2)+...+1=\frac{n (n - 1)}{2} N=(n1)+(n2)+...+1=2n(n1)。交换次数 O ( n ) O(n) O(n),最好情况是,已经有序,交换0次;最坏情况交换 n − 1 n-1 n1次,逆序交换 n 2 \frac{n}{2} 2n次。交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。

#include 

void SelectionSort(int arr[],int nLength){
    if(arr == NULL || nLength <= 0)
        return;
    int i,j;
    int min;
    for (i = 0; i < nLength -1; i++) {
        min = i;
        for (j = i + 1; j < nLength; j++) {
            if (arr[min] > arr[j]) {
                min = j;
            }
        }
        //将最小值放入
        int temp;
        temp = arr[min];
        arr[min] = arr[i];
        arr[i] = temp;
    }
}

void Print(int arr[],int nLength){
    if(arr == NULL || nLength <= 0)
        return;
    for (int i = 0; i < nLength; i++) {
        printf("%d ",arr[i]);
    }
    printf("\n");
}

int main(){
    int arr[] = {10,7,8,19,2,11,66,74,88};
    SelectionSort(arr, sizeof(arr)/sizeof(arr[0]));
    Print(arr, sizeof(arr)/sizeof(arr[0]));
    return 0;
}

插入排序(Insertion Sort)

基本思想是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增 1 的有序表。(也就是:将待排序数据分成两个部分,一部分有序,一部分无序,将无序元素依次插入到有序中去,完成排序)

  • 插入排序的平均时间复杂度是 O ( N 2 ) O(N^2) O(N2),空间复杂度为常数阶 O ( 1 ) O(1) O(1),具体时间复杂度和有序性有关联。

    插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较 N − 1 N-1 N1次,时间复杂度为 O ( N ) O(N) O(N)。最坏的情况是待排序数组是逆序的,此时需要比较次数最多,最坏的情况是 O ( N 2 ) O(N^2) O(N2)

步骤:

1、分为有序/无序俩部分

2、无序插入到有序中去(倒序遍历有序的)

  • (1)保存无序元素
  • (2)倒序遍历,依次比较:小的话则前一个元素向后移动,大的话就把当前元素插入即可
#include 

void InsertionSort(int arr[],int nLength){
    if(arr == NULL || nLength <= 0)
        return;
    int i,j;
    i = 1;
    int temp;
    //无序元素插入
    for (i = 0; i < nLength; i++) {
        j = i - 1;
        temp = arr[i];
        //倒序遍历有序数组 插入无序元素
        //while(temp < arr[j] && j >= 0){ 
        while(j >= 0 && temp < arr[j]){
            //不可能无休止的往前遍历,注意有序数组j限制
            //移动
            arr[j + 1] = arr[j];
            j--;
            //注意 j--后,j = -1时
            //temp < arr[j] && j >= 0中temp < arr[j]会产生越界访问
            //所以需要改成j >= 0 && temp < arr[j]
        }
        //插入
        arr[j + 1] = temp;
    }

}

void Print(int arr[],int nLength){
    if(arr == NULL || nLength <= 0)
        return;
    for (int i = 0; i < nLength; i++) {
        printf("%d ",arr[i]);
    }
    printf("\n");
}

int main(){
    int arr[] = {10,7,8,19,2,11,6,4,88};
    InsertionSort(arr, sizeof(arr)/sizeof(arr[0]));
    Print(arr, sizeof(arr)/sizeof(arr[0]));
    return 0;
}
  • 插入排序适用于:数据少/调整范围小的场景

希尔排序/缩小增量排序(Shell Sort)

内涵了插入排序的思想,以分块的形式放开了数据量的限制,是插入排序的优化体。把记录按下标的一定增量分组,对每组使用直接插入排序算法排序。

  • shell排序的时间复杂度是根据选中的 增量d 有关的,所以分析shell排序的时间复杂度是个比较麻烦的事;只给出答案,不会推算;

    ​ 在最优的情况下,时间复杂度为: O ( N 1.3 ) O(N^{1.3}) O(N1.3) (元素已经排序好顺序)

    ​ 在最差的情况下,时间复杂度为: O ( N 2 ) O(N^2) O(N2)

  • 空间复杂度为常数阶 O ( 1 ) O(1) O(1)

数据结构与算法学习笔记11:二叉树层打印/跳表/冒泡排序/选择排序/插入排序/希尔排序/计数排序_第1张图片

  • 步骤过程:

    1、确定Gap

    2、分组

    ​ 0 0+Gap 0+Gap+Gap…… < n

    ​ 1 1+Gap 1+Gap+Gap…… < n

    ​ ……

    3、各组进行插入排序(此时的调整位数应为+Gap or -Gap)

    4、缩减Gap继续重复23步骤,最后一次分组Gap=1的插入排序进行完后算法结束。

  • 代码:

    #include 
    
    void ShellSort(int arr[],int nLength){
        if(arr == NULL || nLength <= 0)
            return;
        int i,j;
        int nGap;
        int k;
        int temp;
        //确认间隔,此程序以2分方式为例
        for (nGap = nLength / 2; nGap >= 1; nGap /= 2) {
            //组
            for (i = 0; i < nGap; i++) {
                //各组内插入排序
                for (j = i + nGap; j < nLength; j += nGap) {
                    k = j - nGap;   //有序的最后一个元素
                    temp = arr[j];
                    while (k >= i && temp < arr[k]) {
                        arr[k + nGap] = arr[k];    //移动
                        k -= nGap;  //往前走
                    }
                    //插入
                    arr[k + nGap] = temp;
                }
            }
        }
    }
    
    void Print(int arr[],int nLength){
        if(arr == NULL || nLength <= 0)
            return;
        for (int i = 0; i < nLength; i++) {
            printf("%d ",arr[i]);
        }
        printf("\n");
    }
    
    int main(){
        int arr[] = {10,7,8,19,2,11,66,74,88};
        ShellSort(arr, sizeof(arr)/sizeof(arr[0]));
        Print(arr, sizeof(arr)/sizeof(arr[0]));
        return 0;
    }
    
  • 实际上,并不需要一定等第一组执行完后再对第二组排序,多组可以同时进行排序,nGap间隔进行并不相互影响,所以在代码上还可进行一些优化,可以少写一层循环,提高系统的效率,不过时间效率上是一样的。

            for (i = nGap; i < nLength; i++) {
                j = i - nGap;
                temp = arr[i];
                while(j >= 0 && temp < arr[j]){
                    arr[j + nGap] = arr[j];
                    j -= nGap;
                }
                //插入
                arr[j + nGap] = temp;
            }
    

计数排序(Counting Sort )

适用于有一定区间,多重复,数据差值小的情况。是基于非比较的排序

  • 过程:

    1、Max-Min确定区间范围,申请计数器数组

    2、计数

    3、输出

  • 优化:如果是成绩排名,或者有对应情况的,那么只输出值会失去原有信息,所以要优化

    1、Max-Min确定区间范围,申请计数器数组

    2、计数,累加

    3、申请新空间,然后倒序遍历数据,根据计数器数组累加后得到的排名放入对应位置

#include 
#include 
#include 
#include 

void CountingSort(int arr[],int nLength){
    if(arr == NULL || nLength <= 0)
        return;
    //最大值最小值的查找,确定范围
    int nMax = arr[0];
    int nMin = arr[0];
    for (int i = 1; i < nLength; i++) {
        if (arr[i] > nMax) {
            nMax = arr[i];
        }
        if (arr[i] < nMin) {
            nMin = arr[i];
        }
    }
    //计数数组
    int *pCount = NULL;
    pCount = (int*)malloc(sizeof(int)*(nMax - nMin + 1));
    memset(pCount,0,sizeof(int)*(nMax - nMin + 1));
    //计数
    for (int i = 0; i < nLength; i++) {
        pCount[arr[i] - nMin]++;
    }
    //累加
    for (int i = 1; i < nMax - nMin + 1; i++) {
        pCount[i] += pCount[i - 1];
    }
    //申请新空间 存储
    int *pNew = (int*)malloc(sizeof(int)*nLength);
    int index;
    for (int i = nLength - 1; i >= 0; i--) {
        //pNew[pCount[arr[i] - nMin] - 1] = arr[i];
        //pCount[arr[i] - nMin] 累加后得到的排名
        //-1是因为pNew数组下标
        //因为存储完对应计数器要--,所以加一个index
        index = pCount[arr[i] - nMin] - 1;
        pCount[arr[i] - nMin]--;
        pNew[index] = arr[i];
    }

    //pNew就是要得到的排序结果,可以根据情况看要不要拷贝覆盖源数据
    //如果要拷贝
    for (int i = 0; i < nLength; i++) {
        arr[i] = pNew[i];
    }
    free(pNew);
    pNew = NULL;
}

void Print(int arr[],int nLength){
    if(arr == NULL || nLength <= 0)
        return;
    for (int i = 0; i < nLength; i++) {
        printf("%d ",arr[i]);
    }
    printf("\n");
}

int main(){
    int arr[] = {90,87,98,89,92,91,96,94,98};
    CountingSort(arr, sizeof(arr)/sizeof(arr[0]));
    Print(arr, sizeof(arr)/sizeof(arr[0]));
    return 0;
}

你可能感兴趣的:(数据结构与算法学习笔记,学习,排序算法,数据结构)