数据结构——排序

排序

  • 稳定性
  • 内部排序:排序期间元素全部存放在内存中
  • 外部排序:排序期间元素无法同时存在内存中,必须在排序的过程中根据要求不断地在内、外之间移动的排序。

插入排序(Insert Sort)

直接插入排序

思想是,从第二个元素开始,第一个视作已经排序好的序列,不断将元素 在之前排序好的序列中 插入到合适位置,找到合适位置后,这后面的元素均后移。

void InsertSort(int A[], int len) {
   
    int temp;
    for (int i = 1; i < len; len ++) {
   		// 从第二个开始,第一个可视作排序完毕
        if (A[i] < A[i - 1]) {
   				// A[i] 小于前驱,则需要将 A[i] 插入到前面
            temp = A[i];					// 用 temp 暂存 A[i]
            for (int j = i - 1; j >= 0 && A[j] > temp; j--) {
   
            /* 从 i 的前一个开始,直至第一个不满足顺序的元素,将它们全部后移一位,
            注意用 temp 和 A[j] 比较,因为第一次移位后,A[i]便被覆盖了。*/
                A[j + 1] = A[j];
            }
            /* 此时 j 指向的是不满足顺序排序的元素的前一个元素。*/
            A[j + 1] = temp;
        }
    }
}
  • 从第二个元素开始循环,第一个可视作排序完成。

  • if (A[i] < A[i - 1]) 判断这一趟有无必要移动,若有必要,则令 temp = A[i],防止移动时 A[i] 被覆盖后无法获取正确内容。

  • 移动元素时,从当前元素的前驱开始,注意终止条件:j 需合法,并且 要找到不满足顺序的元素。

    结束循环后,最终 j 的位置是不满足顺序的元素的 前驱。

  • 总共有 n - 1 趟,最好时间复杂度为 O(n),最坏时间复杂度为 O(n^2),平均复杂度为 O(n^2)

  • 该算法 稳定


折半插入排序

直接插入的优化

思想是,从第二个元素开始,第一个视作已经排序好的序列,不断将元素 在之前排序好的序列中 插入到合适位置,找合适位置时,使用折半查找,找到合适位置后,这后面的元素均后移。。

void BinaryInsertSort(A[], int len) {
   
    int temp;
    int low, high, mid;
    for (int i = 1; i < len; i ++ ) {
   		// 从第二个开始,第一个可视作排序完毕
        if (A[i] < A[i - 1]) {
   				// A[i] 小于前驱,则需要将 A[i] 插入到前面
            temp = A[i];					// temp 记录 A[i]
            low = 2, high = i - 1, mid = (low + high) / 2;	// 折半初始化
            while (low <= high) {
   			// 折半查找失败信号 low > high
                mid = (low + high) / 2;
                if (A[mid] > temp)			// 在左查找
                   high = mid - 1; 
                if (A[mid] < temp)			// 在右查找
                    low = mid + 1;
                if (A[mid] == temp)			// 特殊之处,相等时,在右查找保证 稳定性
                    low = mid + 1;
            }
            /** 折半结束后,此时 low 指向的下标恰好是需要被插入的位置,所以开始移位
            	移位的起始仍然是 i - 1,终点则确定下来了,一定是 low,
            	此时 for 中不必要写 A[j] > temp了 */
            for (int j = i - 1; j > low; j --) {
   
                A[j + 1] = A[j];
            }
            A[low] = temp;
        }
    }
}
  • 同样的,if (A[i] < A[i - 1]) 是开始排序的“信号”,随后仍要添加 temp = A[i]; ,防止 A[i] 在元素移动后无从找起
  • 保证稳定性,当 A[mid] = temp; 时,意味着前面的序列中出现了与 当前元素相同的 key,此时应该在右边继续查找,即令 low = mid + 1,防止两个元素交换位置。
  • 移动的次数没有变,只是比较关键字次数减少,所以时间复杂度仍然为 O(n^2)
  • 对链表无法实现折半查找,虽然“移动”时间很低,但是由于无法随机存取,时间复杂度仍然是 O(n^2)

在这里插入图片描述


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

在这里插入图片描述

  • 注意 d 是增量,而非“间隔”
void ShellSort(A[], int len) {
   
    // A[0] 只是暂存单元,不是哨兵,当 j <= 0 时,插入位置已到
    int i, j, dk;
    for (dk = len / 2; dk >= 1; dk = dk / 2) {
   	// 每次步长变化
        /** 序列元素从 1 开始,第一个元素可视为排序好的,dk + 1 则刚好是子表中第二个元素
        	每次 i++,切换到“不同的子表”进行 直接插入排序,而非一次性完成一个子表的排序*/
        for (i = dk + 1; i < len; i = i ++) {
   
            if (A[i] < A[i - dk]) {
   				// 子表中该元素比前驱小,则进行 “移位”
                A[0] = A[i];					// 此时的 A[0] 相当于 temp
                for (j = i - 1; j >= 1; j = j - dk) {
   
                    A[j + dk] = A[j];			// 注意移动时,向后移动 dk 
                }
                A[j + dk] = A[0];				// 移动结束后, + dk 才是对应的位置
            }// if								// 该子表该位置前 排序完成
        }
    }
}

/** version 2 更符合算法思想~ */
void ShellSort(A[], int len) {
   
    int i, j, dk, temp;
    for (dk = len / 2; dk >= 1; dk = dk / 2) {
   	// 每次步长变化
        /** 这次用 TableStart 来记录子表的起始位置,每次针对一个子表操作 */
        for (int TableStart = 0; TableStart < dk; TableStart ++) {
   
            /** 同样的从子表第二个元素开始排序 */
            for (i = TableStart + dk; i < len; i = i + dk) {
   
                if (A[i] < A[i - dk]) {
   			// 老规矩,if + temp
                    temp = A[i];
                    /** 注意终止条件是找到 合适的 A[j] */
                    for (j = i - 1; j >= 0 && A[j] > A[0]; j = j - dk) {
   
                        A[j + dk] = A[j];
                    }
                    A[j + dk] = temp;
                }
            }// 一个子表完成
        }// dk 个子表完成
    }// 大 for
}
  • 设定好 d 后,一次完整的 Shell 排序有 d 次直接插入排序。



交换排序

冒泡排序(BubbleSort)

老朋友

  • 外部循环控制趟数,每走一趟,末尾就排序好一个元素,于是少比较一次。
  • 内部循环是每一趟做的事,从第一个开始,每次与后面(j < len -1 防止越界)作比较,符合条件就交换。
// v1 从前往后冒泡~
void BubbleSort(A[], int len) {
   
    bool flag;
    /* 做 len - 1次循环即可,因为每次将 最值 放在最后,放 n - 1 个后,即完成了排序*/
    for (int i = 0; i < len - 1; i ++) {
   
        /** 可以这么理解,从头到尾,两两比较,“交换”即“淘汰”,最终能走向终点的就是胜者——“最值”
        	j 和 后面一个比较,最多只用比较 len - 1 次,否则越界;
        	而每一趟都会在末尾固定好一个“局部最值”,所以下一趟比较次数 - 1
        	则每次只用比较 len - 1 - i次了 */
        for (int j = 0; j < len - i - 1; j ++ ) {
   
            if (A[j] < A[j + 1]) {
   
                swap(A[j], A[j+1]);
                flag = true;			// 有交换发生
            }
        }// 一趟结束
        if (flag == flase)				// 一趟无交换,则已经有序
            break;
    }
}

// v2 从后往前冒泡~邪门儿的很,用 v 1 吧
void BubbleSort (A[], int len) {
   
    bool flag;
    for (int i = 0; i < len - 1; i ++) {
   
        for (int j = len - 1; j > i; j --) {
   
            if (A[j - 1] < A[j]) {
   
                swap(A[j], A[j+1]);
                flag = true;
            }
        }
        if (flag == flase)
            break;
    }
}

在这里插入图片描述


🔸快速排序——务必实践

快速排序每次划分的结果是确定了“基准”元素在序列中的位置,并使得此时序列中基准元素左边元素均小于基准元素,右边均大于基准元素,此时再分别基准左右子序列进行递归,不断确定一个个基准元素,即可完成排序。

在这里插入图片描述

/** 快速排序很像对树进行遍历的思想~ 只可意会不可言传~ */
void QuickSort(*A, int low, int high) {
   
    if (low < high) {
   		// 递归终止条件 low 和 high相等时,仅有一个元素,无需排序了
        /** 对当前序列进行划分——确定 pivot 的位置,然后以此递归 */
        int pivotpos = Partition(A, low, high);
        QuickSort(A, low, pivotpos - 1);	// 依次对子表递归排序
        QuickSort(A, pivotpos + 1, high);
    }
}
/** 核心所在便是 “划分”,通过 low high 不断比较、交换、移动,使得基准元素移动到一个确定位置
	从而以基准元素为界,划分左右子序列*/
int Partition(*A, low, high) {
   
    /** 注意此步,是将初始的 low 位置的元素作为基准,所以 pivot 也记录了此时 low 位置的元素
    	在后续该位置便可以放一个 小于 pivot 的元素,被覆盖掉 */
    ElemType pivot = A[low];
    while (low < high) {
   		// 循环跳出条件,low = high 即意味着找到了基准元素的位置
        
        /** 移动 high,直至找到一个小于 pivot 的元素 
        	一定是先移动 high 指针,出现小于 pivot 的元素,去覆盖“被 pivot 记录过的 A[low]” */
        while (low < high && A[high] >= pivot)
            high--;				// high 前移,当 high = low 时,就确定了 pivot 的位置!
        /** 循环结束有两种情况:
        	1.找到了小于 pivot 的元素,此时固定 high,令该元素前移到 low 的位置
            2.high = low,此时已经找到了 pivot 的位置了,所以 A[low] = A[high] 是自己=自己*/
        /** 第一次的 A[low] 被 pivot 记录过,相当于此位置“空缺”,可以让小于 pivot 的 A[high]
        	移动到这个位置,之后 A[low] 移动给 A[high],也相当于此处“空缺”,
        	可以让循环结束符合条件的 A[high] 移动至此*/
        A[low] = A[high];
        
        
        /** 移动 low,直至找到一个大于 pivot 的元素 */
        while (low < high && A[low] <= pivot)
            low ++;
        /** 循环结束有两种情况:
        	1.找到了大于 pivot 的元素,将其赋值给先前的 high
            2.high = low,此时已经找到了 pivot 的位置了,所以 A[low] = A[high] 是自己=自己*/
        /** 前面的 A[high] 移动到了 low 位置上,此时 high 位置“空缺”,将 A[low] 移动到 high
        	位置,此时 low 位置便“空缺”了,为下一次大 while 的 A[high] 移动做准备*/
        A[high] = A[low];
    }
    A[low] = pivot;				// 最终 low = high,这就是 pivot 的位置
    return low;					// 返回 pivot 的位置
}
  • 递归的终止条件是,子序列中仅有一个元素,即此时low = high

  • 划分的终止条件是,low = high 时,一定找到了基准元素的位置

  • 基准元素(枢轴)取表中第一个元素,即表初始时的 A[low]

  • 划分时,high 指针用来找小于 pivot 的元素,找到后,将它移动到 low 的位置上,此时 high 位置“空缺”;从 low 开始找大于 pivot 的元素,找到后,便可将其移动到“空缺”的 high 位置,而后 low 位置也“空缺”,重复上述过程,直至 low = high 时,便确定了 pivot 的位置。

  • 提升算法效率:

    尽量选取一个可以将数据中分的枢轴元素,如,从序列头尾及中间选取一个“中间值”作为枢轴元素,中间值和 A[low] 交换位置,再令 pivot = A[low],即可转换成上述的程序划分;

    再如,随机选取一个元素作为枢轴元素,处理方法类似。

  • 算法并不稳定

  • 快排的递归并非像归并、块排序那样,递归到最深处,然后层层返回

    快排在第一趟结束后,即确定了一个基准元素的位置

    第二趟时,会确定该基准左右两个子表各自基准元素的位置,此时可把序列分为 4 部分(3 个基准),满足如下关系:

    num < [pivot2_1] < num < [pivot1] < num < [pivot2_2] < num		其中 num 可以是 0 到 多个数字
    

    验证一个序列是否是第二趟的结果,只需验证上述关系(从左向右找,“左边都小,右边都大”,则该元素可以是基准,再从该元素右边起,重复找基准,直到扫描到序列末尾,特别的,当基准为末尾元素时,一定满足【因为右边的子序列递归已经结束了】,便不再需要找基准了)即可。

快速排序递归二叉树

在这里插入图片描述

在这里插入图片描述

  • 把 n 个元素组织成二叉树,二叉树的层数就是递归调用的层数,计算其最小高度和最大高度,即可得知最优和最差空间复杂度。
  • 在上图的二叉树中,一次划分 是在一个子表中确定 “基准” 元素的位置,而 一趟排序 指的是在一整层的子表中确定“基准”元素的位置,所以有 一次划分只确定一个元素位置,一趟排序确定多个元素位置。(从程序看来,这也不难理解!)

时间复杂度和空间复杂度
在这里插入图片描述



选择排序

简单选择排序(SelectSort)

第 i 趟排序:找从下标为 i 到 len - 1 中的最值,将其和下标为 i 的元素交换,重复 n - 1趟即可完成。

/** 王道书,记录下标即可 */
void SelectSort(ElemType A[], int len) {
   
    for (int i = 0; i < len - 1; i ++){
   	// n - 1 趟
        int min = i;					// 记录最小元素位置
        for (int j = i + 1; j < len; j ++) {
   	// 从 i 的后一个开始找 最小元素
            if (A[j] < A[min])			// 找到一个比当前记录的最小值小
                min = j;				// 更新最小元素位置
        }
        if (min != i)					// 一趟结束,如果最初 i 的位置不是最小值
            swap(A[i], A[min]);			// 交换 min 到 i 位置上。
    }
}

/** MHH 版本,记录最小 Value*/
void SelectSort(ElemType A[], int len) {
   
    int minValue;
    for (int i = 0; i < len - 1; i ++) {
   	// 进行 n - 1 次即可排序完成
        for (int j = i; j < len; j++) {
   
            minValue = A[j];
            if (A[j] < minValue){
   
                minValue = A[j];
                swap(A[j], A[i]);
            }
        }
    }
}

在这里插入图片描述


🔸堆排序(HeapSort)

堆排序

  • 大根堆:转化成二叉树,根>左右
  • 小根堆:转化成二叉树,根<左右

在这里插入图片描述

堆的性质和堆排序算法思想

预 备 知 识 : 堆 是 一 种 特 殊 的 二 叉 树 , 而 使 用 顺 序 存 储 的 二 叉 树 ( 从 下 标   1   开 始 ) , 有 以 下 性 质 :

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值