目录

1、问题的引出-求第i个顺序统计量

2、方法一:以期望线性时间做选择

3、方法二(改进):最坏情况线性时间的选择

4、完整测试代码(c++)

5、参考资料

内容

1、问题的引出-求第i个顺序统计量

什么是顺序统计量?及中位数概念

在一个由元素组成的集合里,第i个顺序统计量(order statistic)是该集合第i小的元素。例如,最小值是第1个顺序统计量(i=1),最大值是第n个顺序统计量(i=n)。一个中位数(median)是它所在集合的“中点元素”。当n为奇数时,中位数是唯一的;当n为偶数时,中位数有两个。问题简单的说就是:求数组中第i小的元素。

那么问题来了:如何求一个数组里第i小的元素呢?

常规方法:可以首先进行排序,然后取出中位数。由于排序算法(快排,堆排序,归并排序)效率能做到Θ(nlogn),所以,效率达不到线性;  在本文中将介绍两种线性的算法,第一种期望效率是线性的,第二种效率较好,是在最坏情况下能做到线性效率。见下面两个小节;

2、方法一:以期望线性时间做选择

这是一种分治算法:以快速排序为模型:随机选取一个主元,把数组划分为两部分,A[p...q-1]的元素比A[q]小,A[q+1...r]的元素比A[q]大。与快速排序不同,如果i=q,则A[q]就是要找的第i小 的元素,返回这个值;如果i < q,则说明第i小的元素在A[p...q-1]里;如果i > q,则说明第i小的元素在A[q+1...r]里;然后在上面得到的高区间或者低区间里进行递归求取,直到找到第i小的元素。

下面是在A[p...q]中找到第i小元素的伪码:

  1. RandomSelect(A,p, q,k)//随机选择统计,以期望线性时间做选择
  2. {
  3. if (p==q) return A[p];
  4. int pivot=Random_Partition(A,p,q);//随机选择主元,把数组进行划分为两部分
  5. int i=pivot-p+;
  6. if (i==k )return A[pivot];
  7. else if (i<k) return RandomSelect(A,pivot+,q,k-i);//第k小的数不在主元左边,则在右边递归选择
  8. else return RandomSelect(A,p,pivot-,k);//第k小的数不在主元右边,则在左边递归选择
  9. }

在最坏情况下,数组被划分为n-1和0两部分,而第i个元素总是落在n-1的那部分里,运行时间为Ө(n^2);但是,除了上述很小的概率情况,其他情况都能达到线性;在平均情况下,任何顺序统计量都可以在线性时间Θ(n)内得到。

实现代码(c++):

  1. //template<typename T>使用模板,可处理任意类型的数据
  2. template<typename T>//交换数据
  3. void Swap(T &m,T &n)
  4. {
  5. T tmp;
  6. tmp=m;
  7. m=n;
  8. n=tmp;
  9. }
  10.  
  11. /***********随机快速排序分划程序*************/
  12. template<typename T>
  13. int Random_Partition(vector<T> &A,int p,int q)
  14. {
  15. //随机选择主元,与第一个元素交换
  16. srand(time(NULL));
  17. int m=rand()%(q-p+)+p;
  18. Swap(A[m],A[p]);
  19. //下面与常规快排划分一样
  20. T x=A[p];
  21. int i=p;
  22. for (int j=p+;j<=q;j++)
  23. {
  24. if (A[j]<x)
  25. {
  26. i=i+;
  27. Swap(A[i],A[j]);
  28. }
  29. }
  30. Swap(A[p],A[i]);
  31. return i;
  32. }
  33. /***********随机选择统计函数*************/
  34. template<typename T>
  35. T RandomSelect(vector<T> &A,int p,int q,int k)//随机选择统计,以期望线性时间做选择
  36. {
  37. if (p==q) return A[p];
  38. int pivot=Random_Partition(A,p,q);//随机选择主元,把数组进行划分为两部分
  39. int i=pivot-p+;
  40. if (i==k )return A[pivot];
  41. else if (i<k) return RandomSelect(A,pivot+,q,k-i);//第k小的数不在主元左边,则在右边递归选择
  42. else return RandomSelect(A,p,pivot-,k);//第k小的数不在主元右边,则在左边递归选择
  43. }

3、方法二(改进):最坏情况线性时间的选择

相比于上面的随机选择,我们有另一种类似的算法,它在最坏情况下也能达到O(n)。它也是基于数组的划分操作,而且利用特殊的手段保证每次划分两边的子数组都比较平衡;与上面算法不同之处是:本算法不是随机选择主元,而是采取一种特殊的方法选择“中位数”,这样能使子数组比较平衡,避免了上述的最坏情况(Ө(n^2))。选出主元后,后面的处理和上述算法一致。

那么问题又来了,这种特殊的手段是什么呢?

如上图所示:

1) 将输入数组的n个元素划分为n/5组,每组(上图中的每列为一组)5个元素,且至多只有一个组有剩下的n%5个元素组成

2)  首先对每组中的元素(5个)进行插入排序,然后从排序后的序列中选择出中位数(图中黄色数)。

3) 对第2步中找出的n/5个中位数,递归调用SELECT以找出其中位数x(图中红色数)。(如果有偶数个中位数取较小的中位数)

这三个步骤就可以选出一个很好的主元,下面的处理和方法一一致(递归)

OK! 下面是完整的算法步骤:

1)  将输入数组的n个元素划分为n/5组,每组(上图中的每列为一组)5个元素,且至多只有一个组有剩下的n%5个元素组成

2)  首先对每组中的元素(5个)进行插入排序,然后从排序后的序列中选择出中位数(图中黄色数)。

3) 对第2步中找出的n/5个中位数,递归调用SELECT以找出其中位数x(图中红色数)。(如果有偶数个中位数取较小的中位数)

4) 调用PARTITION过程,按照中位数x对输入数组进行划分。确定中位数x的位置k。

5) 如果i=k,则返回x。否则,如果i<k,则在地区间递归调用SELECT以找出第i小的元素,若干i>k,则在高区找第(i-k)个最小元素。

大致伪码:

  1. WorseLinearSelect(vector<T> &A,int p,int q,int k)
  2. {
  3. // 将输入数组的n个元素划分为n/5(上取整)组,每组5个元素,
  4. // 且至多只有一个组有剩下的n%5个元素组成。
  5. if (p==q) return A[p];
  6.  
  7. int len=q-p+;
  8. int medianCount=;
  9. if (len>)
  10. medianCount = len% > ? len/ + : len/;
  11. vector<T> medians(medianCount);//存放每组的中位数
  12.  
  13. // 寻找每个组的中位数。首先对每组中的元素(至多为5个)进行插入排序,
  14. // 然后从排序后的序列中选择出中位数。
  15. int m=p;
  16. for (int j=,m=p;j<medianCount-;j++)
  17. {
  18. medians[j] = GetMedian(A,m,m+);
  19. m+=;
  20. }
  21. medians[medianCount-] = GetMedian(A,m,q);
  22. //对第2步中找出的n/5(上取整)个中位数,递归调用SELECT以找出其中位数pivot。
  23. //(如果是偶数去下中位数)
  24. int pivot = WorseLinearSelect(medians,,medianCount-,(medianCount+)/);
  25. //调用PARTITION过程,按照中位数pivot对输入数组进行划分。确定中位数pivot的位置r。
  26. int r = partitionWithPivot(A,p,q,pivot);
  27. int num = r-p+;
  28. //如果num=k,则返回pivot。否则,如果k<num,则在地区间递归调用SELECT以找出第k小的元素,
  29. //若干k>num,则在高区找第(k-num)个最小元素。
  30. if(num==k) return pivot;
  31. else if (num>k) return WorseLinearSelect(A,p,r-,k);
  32. else return WorseLinearSelect(A,r+,q,k-num);
  33. }

该算法在最坏情况下运行时间为Θ(n)

代码实现(c++):

  1. template<typename T>//插入排序
  2. void insertion_sort(vector<T> &A,int p,int q)
  3. {
  4. int i,j;
  5. T key;
  6. int len=q-p+;
  7. for (j=p+;j<=q;j++)
  8. {
  9. i=j-;
  10. key=A[j];
  11. while (i>=p&&A[i]>key)
  12. {
  13. A[i+]=A[i];
  14. i--;
  15. }
  16. A[i+]=key;
  17. }
  18. }
  19. /*
  20. * 利用插入排序选择中位数
  21. */
  22. template<typename T>
  23. T GetMedian(vector<T> &A,int p,int q)
  24. {
  25. insertion_sort(A,p,q);//插入排序
  26. return A[(q-p)/ + p];//返回中位数,有两个中位数的话返回较小的那个
  27. }
  28. /*
  29. * 根据指定的划分主元pivot来划分数组
  30. * 并返回主元的顺序位置
  31. */
  32. template<typename T>
  33. int partitionWithPivot(vector<T> &A,int p,int q,T piovt)
  34. {
  35. //先把主元交换到数组首元素
  36. for (int i=p;i<q;i++)
  37. {
  38. if (A[i] == piovt)
  39. {
  40. Swap(A[i],A[p]);
  41. break;
  42. }
  43. }
  44. //常规的快速排序划分程序
  45. //
  46. T x=A[p];
  47. int i=p;
  48. for (int j=p+;j<=q;j++)
  49. {
  50. if (A[j]<x)
  51. {
  52. i=i+;
  53. Swap(A[i],A[j]);
  54. }
  55. }
  56. Swap(A[p],A[i]);
  57. return i;
  58. }
  59. /*
  60. * 最坏情况下线性时间选择算法
  61. * 此算法依然是建立在快速排序的划分算法基础之上的
  62. * 但是与randomizedSelect算法的不同指之处,就是次算法的本质
  63. * 是保证了每次划分选择的划分主元一定是一个较好的主元,算法先对数组5个一组进行分组
  64. * 然后选择每组的中位数,再递归的选择各组中位数中的中位数作为数组的划分主元,以此保证划分的平衡性
  65. * 选择中位数的时候必须使用递归调用的方法才能降低时间复杂度
  66. * 从而保证在最坏情况下都得到一个好的划分
  67. * 最坏情况下时间复杂度为O(n)
  68. */
  69. template<typename T>
  70. T WorseLinearSelect(vector<T> &A,int p,int q,int k)
  71. {
  72. // 将输入数组的n个元素划分为n/5(上取整)组,每组5个元素,
  73. // 且至多只有一个组有剩下的n%5个元素组成。
  74. if (p==q) return A[p];
  75.  
  76. int len=q-p+;
  77. int medianCount=;
  78. if (len>)
  79. medianCount = len% > ? len/ + : len/;
  80. vector<T> medians(medianCount);//存放每组的中位数
  81.  
  82. // 寻找每个组的中位数。首先对每组中的元素(至多为5个)进行插入排序,
  83. // 然后从排序后的序列中选择出中位数。
  84. int m=p;
  85. for (int j=,m=p;j<medianCount-;j++)
  86. {
  87. medians[j] = GetMedian(A,m,m+);
  88. m+=;
  89. }
  90. medians[medianCount-] = GetMedian(A,m,q);
  91. //对第2步中找出的n/5(上取整)个中位数,递归调用SELECT以找出其中位数pivot。
  92. //(如果是偶数去下中位数)
  93. int pivot = WorseLinearSelect(medians,,medianCount-,(medianCount+)/);
  94. //调用PARTITION过程,按照中位数pivot对输入数组进行划分。确定中位数pivot的位置r。
  95. int r = partitionWithPivot(A,p,q,pivot);
  96. int num = r-p+;
  97. //如果num=k,则返回pivot。否则,如果k<num,则在地区间递归调用SELECT以找出第k小的元素,
  98. //若干k>num,则在高区找第(k-num)个最小元素。
  99. if(num==k) return pivot;
  100. else if (num>k) return WorseLinearSelect(A,p,r-,k);
  101. else return WorseLinearSelect(A,r+,q,k-num);
  102. }

4、完整测试代码(c++)

完整源码下载地址Github

Select.h

  1. #ifndef SELECT_HH
  2. #define SELECT_HH
  3. template<typename T>
  4. class Select
  5. {
  6. public:
  7. T RandomSelect(vector<T> &A,int p,int q,int k);//期望线性时间做选择
  8. T WorseLinearSelect(vector<T> &A,int p,int q,int k);//最坏情况线性时间的选择
  9. private:
  10. void Swap(T &m,T &n);//交换数据
  11. int Random_Partition(vector<T> &A,int p,int q);//随机快排分划
  12. void insertion_sort(vector<T> &A,int p,int q);//插入排序
  13. T GetMedian(vector<T> &A,int p,int q);
  14. int partitionWithPivot(vector<T> &A,int p,int q,T piovt);//根据指定主元pivot来划分数据并返回主元的顺序位置
  15. };
  16.  
  17. template<typename T>//交换数据
  18. void Select<T>::Swap(T &m,T &n)
  19. {
  20. T tmp;
  21. tmp=m;
  22. m=n;
  23. n=tmp;
  24. }
  25.  
  26. /***********随机快速排序分划程序*************/
  27. template<typename T>
  28. int Select<T>::Random_Partition(vector<T> &A,int p,int q)
  29. {
  30. //随机选择主元,与第一个元素交换
  31. srand(time(NULL));
  32. int m=rand()%(q-p+)+p;
  33. Swap(A[m],A[p]);
  34. //下面与常规快排划分一样
  35. T x=A[p];
  36. int i=p;
  37. for (int j=p+;j<=q;j++)
  38. {
  39. if (A[j]<x)
  40. {
  41. i=i+;
  42. Swap(A[i],A[j]);
  43. }
  44. }
  45. Swap(A[p],A[i]);
  46. return i;
  47. }
  48. /***********随机选择统计函数*************/
  49. template<typename T>
  50. T Select<T>::RandomSelect(vector<T> &A,int p,int q,int k)//随机选择统计,以期望线性时间做选择
  51. {
  52. if (p==q) return A[p];
  53. int pivot=Random_Partition(A,p,q);//随机选择主元,把数组进行划分为两部分
  54. int i=pivot-p+;
  55. if (i==k )return A[pivot];
  56. else if (i<k) return RandomSelect(A,pivot+,q,k-i);//第k小的数不在主元左边,则在右边递归选择
  57. else return RandomSelect(A,p,pivot-,k);//第k小的数不在主元右边,则在左边递归选择
  58. }
  59.  
  60. template<typename T>//插入排序
  61. void Select<T>::insertion_sort(vector<T> &A,int p,int q)
  62. {
  63. int i,j;
  64. T key;
  65. int len=q-p+;
  66. for (j=p+;j<=q;j++)
  67. {
  68. i=j-;
  69. key=A[j];
  70. while (i>=p&&A[i]>key)
  71. {
  72. A[i+]=A[i];
  73. i--;
  74. }
  75. A[i+]=key;
  76. }
  77. }
  78. /*
  79. * 利用插入排序选择中位数
  80. */
  81. template<typename T>
  82. T Select<T>::GetMedian(vector<T> &A,int p,int q)
  83. {
  84. insertion_sort(A,p,q);//插入排序
  85. return A[(q-p)/ + p];//返回中位数,有两个中位数的话返回较小的那个
  86. }
  87. /*
  88. * 根据指定的划分主元pivot来划分数组
  89. * 并返回主元的顺序位置
  90. */
  91. template<typename T>
  92. int Select<T>::partitionWithPivot(vector<T> &A,int p,int q,T piovt)
  93. {
  94. //先把主元交换到数组首元素
  95. for (int i=p;i<q;i++)
  96. {
  97. if (A[i] == piovt)
  98. {
  99. Swap(A[i],A[p]);
  100. break;
  101. }
  102. }
  103. //常规的快速排序划分程序
  104. //
  105. T x=A[p];
  106. int i=p;
  107. for (int j=p+;j<=q;j++)
  108. {
  109. if (A[j]<x)
  110. {
  111. i=i+;
  112. Swap(A[i],A[j]);
  113. }
  114. }
  115. Swap(A[p],A[i]);
  116. return i;
  117. }
  118. /*
  119. * 最坏情况下线性时间选择算法
  120. * 此算法依然是建立在快速排序的划分算法基础之上的
  121. * 但是与randomizedSelect算法的不同指之处,就是次算法的本质
  122. * 是保证了每次划分选择的划分主元一定是一个较好的主元,算法先对数组5个一组进行分组
  123. * 然后选择每组的中位数,再递归的选择各组中位数中的中位数作为数组的划分主元,以此保证划分的平衡性
  124. * 选择中位数的时候必须使用递归调用的方法才能降低时间复杂度
  125. * 从而保证在最坏情况下都得到一个好的划分
  126. * 最坏情况下时间复杂度为O(n)
  127. */
  128. template<typename T>
  129. T Select<T>::WorseLinearSelect(vector<T> &A,int p,int q,int k)
  130. {
  131. // 将输入数组的n个元素划分为n/5(上取整)组,每组5个元素,
  132. // 且至多只有一个组有剩下的n%5个元素组成。
  133. if (p==q) return A[p];
  134.  
  135. int len=q-p+;
  136. int medianCount=;
  137. if (len>)
  138. medianCount = len% > ? len/ + : len/;
  139. vector<T> medians(medianCount);//存放每组的中位数
  140.  
  141. // 寻找每个组的中位数。首先对每组中的元素(至多为5个)进行插入排序,
  142. // 然后从排序后的序列中选择出中位数。
  143. int m=p;
  144. for (int j=,m=p;j<medianCount-;j++)
  145. {
  146. medians[j] = GetMedian(A,m,m+);
  147. m+=;
  148. }
  149. medians[medianCount-] = GetMedian(A,m,q);
  150. //对第2步中找出的n/5(上取整)个中位数,递归调用SELECT以找出其中位数pivot。
  151. //(如果是偶数去下中位数)
  152. int pivot = WorseLinearSelect(medians,,medianCount-,(medianCount+)/);
  153. //调用PARTITION过程,按照中位数pivot对输入数组进行划分。确定中位数pivot的位置r。
  154. int r = partitionWithPivot(A,p,q,pivot);
  155. int num = r-p+;
  156. //如果num=k,则返回pivot。否则,如果k<num,则在地区间递归调用SELECT以找出第k小的元素,
  157. //若干k>num,则在高区找第(k-num)个最小元素。
  158. if(num==k) return pivot;
  159. else if (num>k) return WorseLinearSelect(A,p,r-,k);
  160. else return WorseLinearSelect(A,r+,q,k-num);
  161. }
  162. #endif

main.cpp

  1. #include <iostream>
  2. #include <vector>
  3. #include <time.h>
  4. using namespace std;
  5. #include "Select.h"
  6. #define N 10 //排序数组大小
  7. #define K 100 //排序数组范围0~K
  8. ////打印数组
  9. void print_element(vector<int> A)
  10. {
  11. int len=A.size();
  12. for (int i=;i<len;i++)
  13. {
  14. std::cout<<A[i]<<" ";
  15. }
  16. std::cout<<std::endl;
  17. }
  18. int main()
  19. {
  20. Select <int> s1;
  21. int a[]={,,,,,,,,,};
  22. vector<int> vec_int(a,a+);
  23. cout<<"原始数组"<<endl;
  24. print_element(vec_int);
  25. // 期望线性时间做选择测试
  26. cout<<"期望线性时间做选择测试"<<endl;
  27. for(int i=;i<=N;i++)
  28. {
  29. int kMin=s1.RandomSelect(vec_int,,N-,i);
  30. cout<<"第"<<i<<"小的数是:"<<kMin<<endl;
  31. }
  32. //最坏情况线性时间的选择测试
  33. cout<<"最坏情况线性时间的选择测试"<<endl;
  34. for(int i=;i<=N;i++)
  35. {
  36. int kMin=s1.WorseLinearSelect(vec_int,,N-,i);
  37. cout<<"第"<<i<<"小的数是:"<<kMin<<endl;
  38. }
  39. system("PAUSE");
  40. return ;
  41. }

5、参考资料

【1】http://blog.csdn.net/xyd0512/article/details/8279371

【2】http://blog.chinaunix.net/uid-26822401-id-3163058.html

【3】http://www.tuicool.com/articles/mqQBfm

【4】http://www.cnblogs.com/Anker/archive/2013/01/25/2877311.html

算法导论-顺序统计-快速求第i小的元素的更多相关文章

  1. 树状数组求第k小的元素

    int find_kth(int k) { int ans = 0,cnt = 0; for (int i = 20;i >= 0;i--) //这里的20适当的取值,与MAX_VAL有关,一般 ...

  2. 利用快排partition求前N小的元素

    求前k小的数,一般人的想法就是先排序,然后再遍历,但是题目只是求前N小,没有必要完全排序,所以可以想到部分排序,而能够部分排序的排序算法我能想到的就是堆排序和快排了. 第一种思路,局部堆排序. 首先, ...

  3. [算法导论]练习2-4.d求排列中逆序对的数量

    转载请注明:http://www.cnblogs.com/StartoverX/p/4283186.html 题目:给出一个确定在n个不同元素的任何排列中逆序对数量的算法,最坏情况需要Θ(nlgn)时 ...

  4. 算法打基础——顺序统计(找第k小数)

    这次主要是讲如何在线性时间下找n个元素的未排序序列中第k小的数.当然如果\(k=1 or k=n\),即找最大最小 数,线性时间内遍历即可完成,当拓展到一般,如中位数时,相关算法就值得研究了.这里还要 ...

  5. 315. Count of Smaller Numbers After Self(二分或者算法导论中的归并求逆序数对)

    You are given an integer array nums and you have to return a new counts array. The counts array has ...

  6. 求第k小的元素

    用快排解决: 用快排,一趟排序后,根据基准值来缩小问题规模.基准值的下角标i 加1 表示了基准值在数组中第几小.如果k<i+1,那就在左半边找:如果k>i+1那就在右半边找.当基准值的下角 ...

  7. 求第 k 小:大元素

    #include<bits/stdc++.h> using namespace std; void swap_t(int a[],int i,int j) { int t=a[i]; a[ ...

  8. 《算法导论》— Chapter 9 中位数和顺序统计学

    序 在算法导论的第二部分主要探讨了排序和顺序统计学,第六章~第八章讨论了堆排序.快速排序以及三种线性排序算法.该部分的最后一个章节,将讨论顺序统计方面的知识. 在一个由n个元素组成的集合中,第i个顺序 ...

  9. 算法导论学习之线性时间求第k小元素+堆思想求前k大元素

    对于曾经,假设要我求第k小元素.或者是求前k大元素,我可能会将元素先排序,然后就直接求出来了,可是如今有了更好的思路. 一.线性时间内求第k小元素 这个算法又是一个基于分治思想的算法. 其详细的分治思 ...

随机推荐

  1. TASK_KILLABLE:Linux 中的新进程状态【转】

    转自:https://www.ibm.com/developerworks/cn/linux/l-task-killable/index.html 新的睡眠状态允许 TASK_UNINTERRUPTI ...

  2. Java反射——java.lang.Class 类简介

    Java的基本思想之一是万事万物即对象,类也是一种对象.但是类是什么对象呢?Java中的类是java.lang.Class的实例化对象,这被成为类类型. //java.lang.Class类中的的主要 ...

  3. Spring -- 注解事务 以及 7个传播行为

    注解事务: 1.开启注解事务配置: <!-- 事务管理器 --> <bean id="transactionManager" class="org.sp ...

  4. 存储过程 ----- navicat 创建存储过程

    以下为navicat 创建存储过程步骤图解: 1. 2. 3. 4. 在存储过程正文中是输入一行语句测试用,点击保存 5.输入存储过程名称,点击确定 6.到这来那么问题来了,会提示错误 7.切记存储过 ...

  5. 图的遍历[DFS][BFS]

    #include<iostream> #include<iostream> #include<cstring> #include<queue> #inc ...

  6. AC日记——魔方 洛谷 P2007

    魔方 思路: 模拟: 代码: #include <cstdio> #include <cstring> #include <iostream> #include & ...

  7. 简单的curl抓取数据

    工欲善其事,必先利其器,数据抓取同样也是如此,PHP数据抓取常用CURL. CURL是一个使用libcurl库与各类服务器进行通讯,支持很多协议,如HTTP.FTP.TELNET等. curl_ini ...

  8. CF 1006B Polycarp's Practice【贪心】

    Polycarp is practicing his problem solving skill. He has a list of n problems with difficulties a1,a ...

  9. Cocos2d-Lua 做一个活动转盘

    这类活动你肯定见过 关于转盘类型的活动我相信大家多多少少都接触到了,很多的抽奖界面都是这类型的,今天这篇小文章就简单的总结一下我们游戏中需要实现这样一个效果的时候我们该怎样去做,其实只要是Cocos类 ...

  10. [BZOJ5461][LOJ#2537[PKUWC2018]Minimax(概率DP+线段树合并)

    还是没有弄清楚线段树合并的时间复杂度是怎么保证的,就当是$O(m\log n)$吧. 这题有一个显然的DP,dp[i][j]表示节点i的值为j的概率,转移时维护前缀后缀和,将4项加起来就好了. 这个感 ...