常用八大排序算法详解
概览:
性能比较:
八大排序算法 | ||||||
---|---|---|---|---|---|---|
类别 | 排序方法 | 时间复杂度 | 空间复杂度 | 稳定性 | ||
平均情况 | 最好情况 | 最坏情况 | 辅助存储 | |||
插入排序 | 直接插入 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
希尔排序 | O(n1.3) | O(n) | O(n2) | O(1) | 不稳定 | |
选择排序 | 直接选择排序 | O(n2) | O(n2) | O(n2) | O(1) | 不稳定 |
堆排序 | O(nlog2n) | O(nlog2n) | O(nlog2n>) | O(1) | 不稳定 | |
交换排序 | 冒泡排序 | O(n2) | O(n) | O(n2) | O(1) | 稳定 |
快速排序 | O(n2) | O(n) | O(n2) | O(nlog2n>) | 不稳定 | |
归并排序 | O(nlog2n>) | nlog2n) | O(nlog2n) | O(1) | 稳定 | |
基数排序 | O(d(r+n)) | O(d(rd+n)) | O(d(r+n)) | O(rd+n) | 稳定 |
稳定性:在正确位置的元素在排序的过程中是否会改变位置,或者说相同大小的元素,它们的相对位置是否在排序后会改变;
1. 插入排序
-
1.1 直接插入排序
- 思路:
将数组看为一副扑克牌,分成左手和右手两边,最初左手只有一张牌,- 从右手中最左端拿一张牌;
- 往这张牌的左边遍历,只要牌面比待插牌点数大,就往后挪一位;
- 重复对比和向右移动,直到找到第一个比待插入牌小的左手牌,将牌插入到它的后面或者找不到这样的牌,插到最左端;
- 重复以上过程,直到将右手的牌全部插入到了左手边,此时整副牌有序;
- Java实现:
public static void insertSort(int[] arr) { //不停地抓牌,从第二张开始 for (int i = 1; i < arr.length; i++) { //记录待插牌的点数 //因为之后牌向右挪占用这里的位置,会覆盖掉值,要继续使用必须记录下来 int insertVal=arr[i]; //从待插牌左边开始比较 int index=i-1; while (index >= 0 && arr[index] > insertVal) { //只要比待插的牌大,就向右挪 arr[index+1]=arr[index]; index--; } //只有index等于-1,或者index处的牌第一次比待插入的小时才会跳出循环 //将牌插入到index的后面(index为0或某处) arr[index+1]=insertVal; } }
- 注意事项
- ⚠️ 比较的停止条件:index >= 0 && arr[index] > insertVal,不能忽略ArrayIndexOutOfBounds的情况;
- ⚠️ 插入的位置:arr[index+1]=insertVal,第一张比待插入牌小的牌无论找到与否,index都刚好在应插入位的前一位;
- 注意事项
- 思路:
-
1.2 希尔排序
- 思路:
希尔排序是直接插入排序的升级版;如果说直接插入排序是对一整副牌进行循环右移并插入的话,那么希尔排序就是将一整副牌分成若干副牌(子副内牌数少),每一副牌内部进行直接插入排序,再将这副已经基本有序的牌分成较少副牌(子副牌数较多),继续操作,继续分更少副牌(子副内牌数更多)并排序,直到只能分出一副牌(包含所有牌)并排序后,已然整体有序; - Java实现:
public static void shellSort(int[] arr) { //⚠️1 使用增量序列{length/2,length/2/2,...,1}进行分组 int increment=arr.length/2; while (increment != 0) { shell(arr, increment); increment/=2; } } private static void shell(int[] arr,int increment){ //⚠️2 i++ NOT i+=increment for (int i = increment; i < arr.length; i++) { int insertVal=arr[i]; int index=i-increment; while (index >= 0 && arr[index] > insertVal) { arr[index + increment] = arr[index]; index-=increment; } arr[index+increment]=insertVal; } }
- 注意事项
- ❓:看上去直接插入排序只进行了一大轮直接插入,而希尔排序却进行了很多轮,为什么希尔排序却比直接插入排序高效呢?
📖:这是因为希尔排序一开始分的子序列多,子序列内元素较少,排序很快,后面分的子序列越来越少,子序列内元素越来越多,排序本应变得困难,但之前的操作已经使得基本有序,所以即使子序列越来越长,排序也仍然很快;而且随着轮数增加,子序列越基本有序; - ❓:如何确定子序列的初始个数以及变化?
📖:采取跳跃式分组的策略,通过某个增量将数组元素划分为若干组;我这里选择增量increment=length/2,缩小增量继续以increment = increment/2的方式,也就是增量序列{n/2, (n/2)/2 …1}。 - ⚠️1:希尔排序的增量序列的选择与证明是个数学难题,我选择的这个增量序列是比较常用的,也是希尔建议的增量,称为希尔增量。
但其实这个增量序列最常用,但却不是最好的,它最坏情形运行时间为O(N2)。其余的增量序列有最坏情形运行时间为O(N3/2)的Hibbard:{1, 3, …, 2k-1},还有更好的Sedgewick:{1, 5, 19, 41, 109…},该序列中的项是9*4i-9*2i + 1或4i-3*2i + 1,这种增量最坏的复杂度为O(N4/3),平均复杂度为O(N7/6),但也没有被完全证明。 - ⚠️2:在子序列排序方法shell()中,控制外层循环的自增是i++,而不是i+=increment,想象中,每个子序列的元素之间间隔了increment,对下一个元素进行操作的时候为什么不是+increment而是+1呢?
这是因为这里并不是把每一个子序列单独拎出再往前插入,而是在同一个循环中对所有的子序列同时操作。只要确保对于任意元素,往前比较时都以increment距离进行,从而确保它所比较的元素与它处于同一子序列内即可,至于下一个操作的元素是哪一个序列中的哪一个元素,不影响操作的进行;
- ❓:看上去直接插入排序只进行了一大轮直接插入,而希尔排序却进行了很多轮,为什么希尔排序却比直接插入排序高效呢?
- 思路:
2.选择排序
-
2.1 直接选择排序
- 思路:
- 在整个序列中寻找最小(大)值所在
- 与最左(右)端的第一个数交换位置
- 然后在除去第一个数的子序列中继续寻找与交换,直到子序列中长度为1,不需要交换为止,至此整个序列已然有序;
- 用公式表达:循环在a[i] ~ a[n]中寻找最小值a[x],将a[x]与a[i]交换,i++。
- Java实现:
- 思路:
public static void selectSort(int[] arr) {
//i只遍历到数组的倒数第二位
for (int i = 0; i < arr.length - 1; i++) {
select(arr,i);
}
}
private static void select(int[] arr, int start) {
int min_index=start;
//i从子序列第二位开始遍历到最后一位
//由于selectSort中已经控制了start最大为数组的倒数第二位,所以start+1不可能越过数组索引边界
for (int i = start+1; i < arr.length; i++) {
if(arr[i]<arr[min_index]) min_index=i;
}
//如果start处就是最大值处,不需要交换
if (min_index != start) {
int temp = arr[start];
arr[start]=arr[min_index];
arr[min_index]=temp;
}
}
-
2.2 堆排序
-
堆:
分为所有父亲节点大于等于孩子节点的大顶堆,和所有父亲节点小于等于孩子节点的小顶堆;
-
思路:
【例】 对于给数组[4,6,8,5,9]升序排序;
1.将无序序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
2.我们把它看作一棵二叉树,然后试图将它改造成大顶堆;
从最后一个非叶子结点开始1开始,把以它为结点的子树也调整成大顶堆:从上图看出,它和它的左右孩子中最大的是4号,所以1号结点和4号节点的值交换,如下图:
其实这里应该检查交换会不会影响子节点是否仍然是大顶堆,但由于子节点3和4都是叶子结点,肯定是不会影响的;
3.然后再去倒数第二个非叶子结点0号,对它进行调整,从上图看出,它和左右孩子中最大的是左孩子1号,所以0号和1号的值交换,如下图;
由于子节点1号值变更了,而且它有子节点,意味着它很可能不在是它和它孩子中最大的,要进行调整;
4.对1号节点进行调整,从上图得知,它和它的孩子中最大的是4号节点,所以1和4互换值;
并且再次需要对被改变的4号元素进行调整,而这里4号节点是叶子结点,不可能因为值改变而受影响;
至此,第一趟大顶堆构造完毕;
5.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
6.将沉至末尾的4号元素忽略,不参与接下来的构建,剩下的元素重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素(除去上一个末尾元素),反复执行调整+交换步骤,直到整个序列有序。
7.第二次调整:
第三次调整:
第四次调整:至此,剩下的元素只有一个,已经没有非叶子结点了,已经按升序有序;
-
Java实现
public static void heapSort(int[] arr) {
if (arr == null || arr.length == 0) {
return;
}
int len=arr.length;
//构建大顶堆
//⚠️1+⚠️2+⚠️3:从最后一个非叶子节点往前遍历每一个非叶子节点
for (int i = len / 2 - 1; i >= 0; i--) {
//把非叶子节点i为根节点的二叉树调整成大顶堆
heapAdjust(arr,i,len);
}
//大顶堆首次构建完成后
for (; len > 1; len--) {
//将大顶堆中的最大值沉到末端(根节点和尾节点数值互换)
swap(arr,0,len-1);
//将最大值所在节点忽略不计,剩余节点继续要调整成大顶堆,从而可以获得下一个最大值
//剩余节点必须大于2个才有调整的必要,len-1>=2
if (len >= 3) {
//由于只变动了首元素大小,所以直接从首节点向下调整起
//不能忘记末尾节点已经不参与构建了,len-1
heapAdjust(arr,0,len-1);
}
}
}
private static void heapAdjust(int[] arr, int index,int len) {
//待调整根节点的左孩子和右孩子存在的话,应是的下标
int left = 2 * index + 1;
int right = 2 * index + 2;
//默认待调整节点、左孩子、右孩子中最大的是待调整节点
int max_index=index;
//如果左孩子存在,且左孩子大于此时记录的最大值,就将记录改成左孩子下标
if (left < len && arr[left] > arr[max_index]) max_index=left;
//如果右孩子存在,且右孩子大于此时记录的最大值,就将记录改成右孩子下标
if (right < len && arr[right] > arr[max_index]) max_index=right;
//如果最大值下标和先开始的不同,说明左孩子和右孩子中有大于根节点的,需要调整
if (max_index != index) {
//将最大的那个孩子与根节点互换值
swap(arr,index,max_index);
//该孩子值产生变动,可能会使它与它的左右孩子不满足大顶堆,继续对该孩子进行调整
heapAdjust(arr,max_index,len);
}
}
private static void swap(int[] arr, int a, int b) {
int temp = arr[a];
arr[a] = arr[b];
arr[b]=temp;
}
- 注意事项:
- ⚠️1:我们可以看出,由于虚拟成二叉树是从上到下,从左到右构建的,那么对于一个下标是i的元素来说,如果它有子节点,那么它前面那i个元素必定都有了两个子节点,所以它的左孩子下标是2i+1,右孩子下标2i+2;
- ⚠️2:由于最后一个非叶子结点一定是末尾元素的父节点;假设它的下标为i,结合⚠️1,如果它有两个孩子(元素总数为偶),那么末尾元素的下标就会是2i+2,如果它只有一个孩子(元素总数为奇),末尾元素下标就会是2i+1;而已知末尾元素下标是length-1,所以求得i=length(奇)/2-1,或length(偶)/2-3/2,但计算机中除法是向下取整的,也就是说length(奇)/2=(length(奇)-1)/2,那么i=length(奇)/2-3/2,所以i可以直接由length/2-1通用表示;
- ⚠️3:最后一个非叶子结点找到了后,所有下标小于它的结点都是非叶子结点;这是因为这里二叉树是从上到下,从左到右构建的,对于某个节点来说,只有排在它前面的节点左右孩子都被构建了,才轮得到它拥有孩子;那么既然它是非叶子结点了,那么它前面的节点必定都是非叶子结点;
3.交换排序
-
3.1 冒泡排序
- 思路:将数组内元素看成一个个大小不一的泡泡;
- 从最下面的泡泡开始,它如果比它上面的泡泡大,就会“浮”到它的上方(与它上面的泡泡互换位置");
- 再对接下来的每一个进行同样的比较;
- 对倒数第二个泡泡比较完后,最上面的泡泡就是最大的泡泡;
- 将最大的泡泡当作不存在,对剩余的泡泡继续以上的操作,逐渐第二大、第三大的泡泡都会归位;
- 直到需要操作的泡泡只剩下一个时,整个泡泡序列有序;
- Java实现
1.递归实现
2.普通实现public static void bubbleSort(int[] arr) { if (arr == null || arr.length == 0) { return; } bubble(arr,arr.length); } private static void bubble(int[] arr, int len) { if(len<2) return; int temp; for (int i = 0; i < len - 1; i++) { if (arr[i] > arr[i + 1]) { temp = arr[i]; arr[i] = arr[i + 1]; arr[i + 1]=temp; } } bubble(arr, len - 1); }
3.优化public static void bubbleSort(int[] arr) { for (int i = 1; i < arr.length; i++) { for (int j = 0; j < arr.length - i; j++) { if (arr[i] > arr[i + 1]) swap(arr, i, i + 1); } } }
public static void bubbleSort(int[] arr) { boolean change; for (int i = 1; i < arr.length; i++) { change=false; for (int j = 0; j < arr.length - i; j++) { if (arr[i] > arr[i + 1]) { swap(arr, i, i + 1); change=true; } } if(!change) return; } }
- 注意事项:
- ⚠️1: 以上三种实现方式,递归和普通方式,最好复杂度是O(n2),但优化方式多了一个标记,以在循环没有进行交换的时候,提前结束,如果数组本就是正序,只需要进行n次判断是否需要交换即可;
- ⚠️2:内外循环的循环变量起始位置需要注意;
- 思路:将数组内元素看成一个个大小不一的泡泡;
-
3.2 快速排序
- 思路:是冒泡排序的加强版,冒泡排序是从一头向另一头冒,而快速排序是向两头操作;
- 选择一个数(一般为最左边的数);
- 先从右边遍历起,将第一个比它小的数与它交换位置;
- 再从左边遍历,将第一个比它大的数与它交换位置;
- 重复以上操作,直到从左往右的索引和从右往左的索引碰到一起,这时对于这个数而言,比它小的都在它左边,比它大的都在它右边;
- 对于它左右两边的序列分别执行以上所有操作(包括本步骤),直到子序列都无法继续细分为止;
- 至此,微观上,对于每个元素,小于它的数都在左边,大于它的数都在右边,宏观上整个序列有序;
- Java实现:
public void quickSort(int[] arr, int start, int end) { //基准值 int low=start; int high=end; //如果这个序列的元素个数大于1的话 if (start < end) { //只要low和high没碰面 while (low < high ) { //从右边往左遍历,寻找比基准值小的数 //一直没找到就一直递减,直到把基准值右边的数找完 while (arr[high] > arr[low]) { high--; } //找到了就交换,找不到时也会因为low=high(因为这时arr[low]=arr[high],跳出循环),交换也不影响 //因为low处交换过后,已经是比基准值小的值了,接下来从左往右寻找比基准值大的数的时候,它不可能满足条件,没有遍历的必要,所以交换后,low++ swap(arr, low++, high); //从左边往右遍历,寻找比基准值大的数 //一直没找到就一直递增,直到把基准值左边的数找完 while (arr[low] < arr[high]) { low++; } //找到了就交换,找不到时也会因为low=high,交换也不影响 //因为high处交换过后,已经是比基准值小的值了,接下来从左边寻找比基准值大的值的时候,它不可能满足条件,没有遍历的必要,所以交换后,high-- swap(arr, low, high--); } //将调整好的基准值左右序列也同样操作 quickSort(arr, start, low-1); quickSort(arr, high + 1, end); } }
- 注意事项:
- ⚠️:从右边找到了更小的值换到了左边时,一定要记得将low++;同样地,从左边交换过来值后,右边的high–,这样能减少不必要的遍历;
- 思路:是冒泡排序的加强版,冒泡排序是从一头向另一头冒,而快速排序是向两头操作;
4. 归并排序
- 思路:
- 也叫二路归并排序,将序列不停分为两半,各自排序好再归并到一起;
- 使用递归的方式实现;递归公式在前,排序操作在后,所以会先递进到二路划分的最小序列上,在小序列排序好之后,回归到较大序列中进行再排序;
- 由于这样的操作,较大序列排序时,只需要将两个有序小序列组装到一起,这种组装的实现比起乱序排序要简单的多;
- Java实现:
public static void mergeSort(int[] arr) {
if (arr == null || arr.length == 0) return;
merge(arr, 0, arr.length - 1);
}
private static void merge(int[] arr, int left, int right) {
//只要序列长度不为1,就进行二路归并
if (left < right) {
int mid=left+(right-left)/2;
//将左子序列二路归并
merge(arr, left, mid);
//将右子序列二路归并
merge(arr, mid + 1,right);
//将左右子序列合并组装
combine(arr,left,mid,right);
}
}
private static void combine(int[] arr, int left,int mid, int right) {
//使用一个临时数组给两个子序列排序用以及创建遍历它的索引,和遍历左右序列的两个索引
int[] temps = new int[right - left + 1];
int index=0;
int index_left=left;
int index_right=mid+1;
//只有左边遍历完了或右边遍历完了才停止
while (index_left <= mid && index_right <= right) {
//如果左索引处数小于右索引处数,将左索引处数拷贝到临时数组
//左索引和临时数组索引+1
if (arr[index_left] < arr[index_right]) {
temps[index++]=arr[index_left++];
}else{
//否则右索引处数拷贝到临时数组中,两个索引+1
temps[index++]=arr[index_right++];
}
}
//有可能某一边的子序列没有全部遍历完,另一边就遍历完而结束比较了,此时要把剩下的那些元素也放入临时数组
//左序列没遍历完
while (index_left <= mid) {
temps[index++] = arr[index_left++];
}
//右序列没遍历完
while (index_right <= right) {
temps[index++] = arr[index_right++];
}
//将临时节点的数覆盖到原数组中
for (int i = 0; i < temps.length; i++) {
arr[left++] = temps[i];
}
}
5. 基数排序
-
5.1 计数排序
-
思想: 由于数组下标天然有序,在需要对某个序列排序时,将它的每个数值在等值下标处(或相对最小值偏移量处)做上标记,再对这个数组顺序遍历,将被标记的地方输出,就做到了给原序列排序;
考虑到序列中可能会有重复的元素,所以将标记设置为这个元素出现的次数,输出时按这个标记的次数多次输出; -
Java实现:
public static void countSort(int[] arr) { //求最大、最小值,以确定临时数组大小 int max=Integer.MIN_VALUE; int min=Integer.MAX_VALUE; for (int i = 0; i < arr.length; i++) { if(arr[i]>max) max=arr[i]; if(arr[i]<min) min=arr[i]; } //创建临时数组,并构建记录 int[] temps=new int[max-min+1]; for (int i = 0; i < arr.length; i++) { temps[arr[i]]++; } //将临时数组输出到原数组中进行覆盖 for (int i = 0, j = 0; i < temps.length; i++) { //只要此处计数不为0,就一直使用这个下标值在原数组中覆盖 while (temps[i] != 0) { arr[j++]=i; temps[i]--; } } }
- 注意事项
- 🤔:如果待排序序列中有小数或者负数这种元素,就无法利用数组下标了,计数排序不再可用;
另外,计数排序是以序列的极差为长度创建临时数组的,那么如果序列的极差很大,但待排序元素分布极不均匀,如[1,3,5,4,9999]需要创建一个长度为9999的临时数组,但其实其中只有5处参与了计数且集中在1~5,6~9998的空间没有使用,还在输出的时候白白进行遍历,这就造成了空间和时间上的极大浪费;
所以,计数排序只适用于元素是自然数,且分布均匀的序列;
- 🤔:如果待排序序列中有小数或者负数这种元素,就无法利用数组下标了,计数排序不再可用;
-
-
5.2 桶排序
-
思想:如果序列极差过大或者元素非自然数时,计数排序不适用,这时,引出了桶排序;
如果把临时数组中每一格看作一个桶的话,计数排序的每个桶只能放一种元素和它的数量,而桶排序是将序列元素的范围以某种方式分配给各个桶,再将每个元素分配到对应范围的桶中,然后对每个非空桶进行桶内排序,再把所有排序好的非空桶输出覆盖到原数组,就完成了整个排序流程; -
Java实现:
public static void bucketSort(int[] arr) { int min=Integer.MAX_VALUE; int max=Integer.MIN_VALUE; for (int i = 0; i < arr.length; i++) { if(arr[i]<min) min=arr[i]; if(arr[i]>max) max=arr[i]; } //⚠️1:创建桶,如何确定桶的数量? ArrayList<ArrayList<Integer>> buckets = new ArrayList<>(arr.length); //⚠️1:如何确定每个桶内区间跨度? int section=(max-min)/(arr.length-1); for (int i = 0; i < arr.length; i++) { buckets.add(new ArrayList<Integer>(section)); } //将原数组的每个元素放桶内 for (int i = 0; i < arr.length; i++) { //⚠️1:如何确定映射方法? int j=(arr[i]-min)/section; buckets.get(j).add(arr[i]); } //每个桶内做桶内排序(我们可以相信JDK的排序速度) //并将这个桶内元素覆盖回原数组 int index=0; for (ArrayList<Integer> bucket : buckets) { Collections.sort(bucket); for (Integer integer : bucket) { arr[index++]=integer; } } }
- 注意事项
- ⚠️1:如何确定每个桶划分的范围呢?
📖:具体建立多少个桶,如何确定桶的区间范围,有很多不同的映射规则。若规则设计的过于模糊、宽泛,则可能导致待排序集合中所有元素全部映射到一个桶上,则桶排序向比较性质排序算法演变。若映射规则设计的过于具体、严苛,则可能导致待排序集合中每一个元素值映射到一个桶上,则桶排序向计数排序方式演化。
我们这里创建的桶数量等于原始数列的元素数量(应对每个元素都对应一个桶的极端情况),除了最后一个桶只包含数列最大值,前面各个桶的区间按照比例确定。
- ⚠️1:如何确定每个桶划分的范围呢?
-
-
5.3 基数排序
-
思想:
尽管桶排序对于计数排序可能存在的序列极差过大而导致临时数组过大的问题已经做出了改善,但桶的数量仍然与原序列长度呈正相关;那么有没有一种方案,仅仅通过固定个数的桶,就能处理任意长度的序列呢?
于是,基数排序出场了!(终于要写完了)
无论序列里的元素大小如何,都是由个、十、百、千…的数字组成,而且数字的范围是0~9,可想而之,我们可以创建0~9的10个桶,然后分别依据个位、十位、百位…将序列元素往桶中放置,放置完一轮就输出覆盖一次,直到序列中没有更高位可以依据时,整个序列有序;
-
Java实现:
public static void radixSort(int[] arr) { //创建桶 int[][] buckets=new int[10][arr.length]; //将元素多次入桶 //⚠️1:记录每个桶内元素数量 int[] count; //找到序列中最大值 int max=Integer.MIN_VALUE; for (int i = 0; i < arr.length; i++) { if(arr[i]>max) max = arr[i]; } //得到最大值的位数,以控制入桶次数 max=(max+"").length(); for (int i = 0; i < max; i++) { //⚠️2 count=new int[10]; for (int j = 0; j < arr.length; j++) { //个位数求法:n%10,十位数求法:n/10%10,百位数求法:n/100%10 int bucket_id=arr[j]/(int)Math.pow(10,i)%10; buckets[bucket_id][count[bucket_id]++]=arr[j]; } //每入完一次桶,进行一次输出覆盖 for (int j = 0, index = 0; j < count.length; j++) { if (count[j] != 0) { for (int p = 0; p < count[j]; p++) { arr[index++]=buckets[j][p]; } } } } }
- 注意事项:
- ⚠️1:为什么要创建一个数组来记录桶内元素的数量?
📖:因为这里是使用二维数组来实现桶,即使没有元素入桶,也会存在若干个初始的0,如果不记录真正的序列元素的数量,不管是往桶内增加元素还是之后从桶内输出元素都没有一个确切的位置可供操作;
当然,如果采用上一节桶排序中使用ArrayList来实现桶的话,调用add方法增加元素,使用ArrayList自己的遍历方法(普通for、增强for或迭代器)进行元素输出即可,这时就不另外需要额外的计数数组;
⚠️2:在使用了计数数组的情况中,在每一次把桶中元素输出到上一个版本的数组之后,进行下一个版本的入桶之前,需要将计数数组内的计数初始化,否则会残留上一轮的计数信息;
- ⚠️1:为什么要创建一个数组来记录桶内元素的数量?
-