أسرع خوارزمية لمصفوفة ذات حجم N للإزاحة الدائرية للموضع M

StackOverflow https://stackoverflow.com/questions/876293

سؤال

ما هي أسرع خوارزمية لمصفوفة تبديل الدائرة للمواضع M؟
على سبيل المثال، [3 4 5 2 3 1 4] التحول M = ينبغي أن يكون موقفين [1 4 3 4 5 2 3].

شكرًا جزيلاً.

هل كانت مفيدة؟

المحلول

إذا كنت تريد O (ن) وقت وليس استخدام الذاكرة اضافية (منذ تم تحديد مجموعة)، استخدام خوارزمية من كتاب جون بنتلي، "برمجة اللؤلؤ 2 الطبعة". انها مقايضة جميع العناصر مرتين. ليست سريعة مثل استخدام القوائم المرتبطة ولكن يستخدم ذاكرة أقل وبسيطة من الناحية النظرية.

shiftArray( theArray, M ):
    size = len( theArray )
    assert( size > M )
    reverseArray( theArray, 0, size - 1 )
    reverseArray( theArray, 0, M - 1 )
    reverseArray( theArray, M, size - 1 )

وreverseArray (anArray، startIndex، endIndex) عكس ترتيب العناصر من startIndex إلى endIndex، ضمنا.

نصائح أخرى

وانها مجرد مسألة التمثيل. إبقاء المؤشر الحالي كما متغير عدد صحيح وعندما يجتاز مجموعة المشغل استخدام مودولو أن تعرف متى التفاف حولها. تحول بعد ذلك فقط تغيير قيمة المؤشر الحالي، التفاف حول حجم المصفوفة. وهذا بالطبع O (1).

وعلى سبيل المثال:

int index = 0;
Array a = new Array[SIZE];

get_next_element() {
    index = (index + 1) % SIZE; 
    return a[index];
}

shift(int how_many) {
    index = (index+how_many) % SIZE;
}

حل مثالي

السؤال المطروح للأسرع.يعد العكس ثلاث مرات هو الأسهل ولكنه يحرك كل عنصر مرتين بالضبط، ويستغرق وقت O(N) ومسافة O(1).من الممكن إجراء إزاحة دائرية لمصفوفة تتحرك كل عنصر مرة واحدة بالضبط أيضًا في وقت O(N) ومساحة O(1).

فكرة

يمكننا دائرة التحول مجموعة من الطول N=9 بواسطة M=1 مع دورة واحدة:

tmp = arr[0]; arr[0] = arr[1]; ... arr[7] = arr[8]; arr[8] = tmp;

و إذا N=9, M=3 يمكننا دائرة التحول بثلاث دورات:

  1. tmp = arr[0]; arr[0] = arr[3]; arr[3] = tmp;
  2. tmp = arr[1]; arr[1] = arr[4]; arr[4] = tmp;
  3. tmp = arr[2]; arr[2] = arr[5]; arr[5] = tmp;

لاحظ أن كل عنصر تتم قراءته مرة واحدة وكتابته مرة واحدة.

رسم تخطيطي للتحول N=9, M=3

Diagram of cycle shift

تظهر الدورة الأولى باللون الأسود مع أرقام تشير إلى ترتيب العمليات.وتظهر الدورتين الثانية والثالثة باللون الرمادي.

عدد الدورات المطلوبة هو القاسم المشترك الأكبر (جي سي دي) من N و M.إذا كان GCD هو 3، نبدأ دورة في كل منها {0,1,2}.حساب GCD سريع مع خوارزمية GCD الثنائية.

رمز المثال:

// n is length(arr)
// shift is how many place to cycle shift left
void cycle_shift_left(int arr[], int n, int shift) {
  int i, j, k, tmp;
  if(n <= 1 || shift == 0) return;
  shift = shift % n; // make sure shift isn't >n
  int gcd = calc_GCD(n, shift);

  for(i = 0; i < gcd; i++) {
    // start cycle at i
    tmp = arr[i];
    for(j = i; 1; j = k) {
      k = j+shift;
      if(k >= n) k -= n; // wrap around if we go outside array
      if(k == i) break; // end of cycle
      arr[j] = arr[k];
    }
    arr[j] = tmp;
  }
}

الكود في C لأي نوع صفيف:

// circle shift an array left (towards index zero)
// - ptr    array to shift
// - n      number of elements
// - es     size of elements in bytes
// - shift  number of places to shift left
void array_cycle_left(void *_ptr, size_t n, size_t es, size_t shift)
{
  char *ptr = (char*)_ptr;
  if(n <= 1 || !shift) return; // cannot mod by zero
  shift = shift % n; // shift cannot be greater than n

  // Using GCD
  size_t i, j, k, gcd = calc_GCD(n, shift);
  char tmp[es];

  // i is initial starting position
  // Copy from k -> j, stop if k == i, since arr[i] already overwritten
  for(i = 0; i < gcd; i++) {
    memcpy(tmp, ptr+es*i, es); // tmp = arr[i]
    for(j = i; 1; j = k) {
      k = j+shift;
      if(k >= n) k -= n;
      if(k == i) break;
      memcpy(ptr+es*j, ptr+es*k, es); // arr[j] = arr[k];
    }
    memcpy(ptr+es*j, tmp, es); // arr[j] = tmp;
  }
}

// cycle right shifts away from zero
void array_cycle_right(void *_ptr, size_t n, size_t es, size_t shift)
{
  if(!n || !shift) return; // cannot mod by zero
  shift = shift % n; // shift cannot be greater than n
  // cycle right by `s` is equivalent to cycle left by `n - s`
  array_cycle_left(_ptr, n, es, n - shift);
}

// Get Greatest Common Divisor using binary GCD algorithm
// http://en.wikipedia.org/wiki/Binary_GCD_algorithm
unsigned int calc_GCD(unsigned int a, unsigned int b)
{
  unsigned int shift, tmp;

  if(a == 0) return b;
  if(b == 0) return a;

  // Find power of two divisor
  for(shift = 0; ((a | b) & 1) == 0; shift++) { a >>= 1; b >>= 1; }

  // Remove remaining factors of two from a - they are not common
  while((a & 1) == 0) a >>= 1;

  do
  {
    // Remove remaining factors of two from b - they are not common
    while((b & 1) == 0) b >>= 1;

    if(a > b) { tmp = a; a = b; b = tmp; } // swap a,b
    b = b - a;
  }
  while(b != 0);

  return a << shift;
}

يحرر:قد تتمتع هذه الخوارزمية أيضًا بأداء أفضل مقابل عكس المصفوفة (متى N كبير و M صغير) نظرًا لموقع ذاكرة التخزين المؤقت، نظرًا لأننا نقوم بالتكرار فوق المصفوفة بخطوات صغيرة.

ملاحظة أخيرة: إذا كانت مصفوفتك صغيرة، فإن العكس الثلاثي يكون بسيطًا.إذا كان لديك مصفوفة كبيرة، فمن المفيد العمل على GCD لتقليل عدد الحركات بعامل 2.المرجع: http://www.geeksforgeeks.org/array-rotation/

وإعداده مع مؤشرات، ويستغرق وقتا لا تقريبا. كل نقطة عنصر إلى آخر، و "آخر" (لا يوجد آخر، وبعد كل شيء، وقال لكم انه كان التعميم) نقطة للأول. مؤشر واحد إلى "بداية" (العنصر الأول)، وربما طول، وكان لديك مجموعة الخاصة بك. الآن، على أن تفعل التحول الخاص بك، فإنك مجرد المشي المؤشر بداية على طول الدائرة.

واسأل عن خوارزمية جيدة، ويمكنك الحصول على أفكار معقولة. طلب <م> أسرع ، ويمكنك الحصول على أفكار غريبة!

وهذه الخوارزمية يعمل في O (ن) الوقت وO (1) الفضاء. والفكرة هي أن تتبع كل مجموعة دوري في التحول (مرقمة من قبل متغير nextGroup).

var shiftLeft = function(list, m) {
    var from = 0;
    var val = list[from];
    var nextGroup = 1;
    for(var i = 0; i < list.length; i++) {
        var to = ((from - m) + list.length) % list.length;
        if(to == from)
            break;

        var temp = list[to];
        list[to] = val;
        from = to;
        val = temp;

        if(from < nextGroup) {
            from = nextGroup++;
            val = list[from];
        }
    }
    return list;
}
def shift(nelements, k):       
    result = []
    length = len(nelements)
    start = (length - k) % length
    for i in range(length):
        result.append(nelements[(start + i) % length])
    return result

وهذا الرمز يعمل بشكل جيد حتى على ك تحول سلبي

وظيفة C arrayShiftRight. إذا تحول سلبي تركت مجموعة التحولات وظيفة. هو الأمثل لأقل من استخدام الذاكرة. دوران وقت O (ن).

void arrayShiftRight(int array[], int size, int shift) {
    int len;

    //cut extra shift
    shift %= size;

    //if shift is less then 0 - redirect shifting left
    if ( shift < 0 ) {
        shift += size;
    }

    len = size - shift;

    //choosing the algorithm which needs less memory
    if ( shift < len ) {
        //creating temporary array
        int tmpArray[shift];

        //filling tmp array
        for ( int i = 0, j = len; i < shift; i++, j++ ) {
            tmpArray[i] = array[j];
        }

        //shifting array
        for ( int i = size - 1, j = i - shift; j >= 0; i--, j-- ) {
            array[i] = array[j];
        }

        //inserting lost values from tmp array
        for ( int i = 0; i < shift; i++ ) {
            array[i] = tmpArray[i];
        }
    } else {
        //creating temporary array
        int tmpArray[len];

        //filling tmp array
        for ( int i = 0; i < len; i++ ) {
            tmpArray[i] = array[i];
        }

        //shifting array
        for ( int i = 0, j = len; j < size; i++, j++ ) {
            array[i] = array[j];
        }

        //inserting lost values from tmp array
        for ( int i = shift, j = 0; i < size; i++, j++ ) {
            array[i] = tmpArray[j];
        }
    }
}

وهناك حل بسيط جدا. هذا هو وسيلة سريعة جدا، وهنا يمكنني استخدام مجموعة ودرجة الحرارة مع نفس الحجم أو الأصلي ونعلق على المتغير الأصلي في نهاية المطاف. هذا الاستخدام طريقة O (ن) التعقيد الزمني وO (ن) مساحة التعقيد وبسيط جدا لتنفيذ.

int[] a  = {1,2,3,4,5,6};
    int k = 2;
    int[] queries = {2,3};

    int[] temp = new int[a.length];
    for (int i = 0; i<a.length; i++)
        temp[(i+k)%a.length] = a[i];

    a = temp;

واعتمادا على بنية البيانات التي تستخدمها، يمكنك أن تفعل ذلك في O (1). أعتقد أن أسرع وسيلة لعقد مجموعة في شكل قائمة مرتبطة، ويكون لها جدول التجزئة التي يمكن أن تترجم بين "مؤشر" في مجموعة ل"مؤشر" للدخول. وبهذه الطريقة يمكنك العثور على رؤوس وذيول ذات الصلة في O (1)، والقيام إعادة الاتصال في O (1) (وتحديث جدول التجزئة بعد التبديل في O (1)). وهذا من شأنه بالطبع أن يكون "فوضوي" حل جدا، ولكن إذا كان كل ما كنت ترغب في هو سرعة التحول، من شأنها أن تفعل (على حساب الإدراج أطول والبحث في مجموعة، ولكنه سوف لا تزال O ( 1))

إذا كان لديك البيانات في مجموعة نقية، لا أعتقد يمكنك تجنب O (ن).

والترميز الحكيم، فإنه يعتمد على ما هي اللغة التي تستخدمها.

في بيثون على سبيل المثال، هل يمكن أن "شريحة" عليه (يفترض n هو حجم التحول):

result = original[-n:]+original[:-n]

(وأنا أعلم أن تجزئة بحث في نظرية لا O (1) ولكن نحن عملي هنا وليس النظري، على الأقل آمل ذلك ...)

وهذا يجب أن تعمل لتحويل لدائري arry: المدخلات: {1، 2، 3، 5، 6، 7، 8}؛ قيمة الانتاج الحالية في مجموعة بعد forloops: {8،7،1،2،3،5،6،8،7}

 class Program
    {
        static void Main(string[] args)
        {
            int[] array = { 1, 2, 3, 5, 6, 7, 8 };
            int index = 2;
            int[] tempArray = new int[array.Length];
            array.CopyTo(tempArray, 0);

            for (int i = 0; i < array.Length - index; i++)
            {
                array[index + i] = tempArray[i];
            }

            for (int i = 0; i < index; i++)
            {
                array[i] = tempArray[array.Length -1 - i];
            }            
        }
    }

إليك وظيفة تدوير عامة بسيطة وفعالة في لغة C++، أقل من 10 أسطر.

وهو مقتطف من إجابتي على سؤال آخر. كيفية تدوير مجموعة؟

#include <iostream>
#include <vector>

// same logic with STL implementation, but simpler, since no return value needed.
template <typename Iterator>
void rotate_by_gcd_like_swap(Iterator first, Iterator mid, Iterator last) {
    if (first == mid) return;
    Iterator old = mid;
    for (; mid != last;) {
        std::iter_swap(first, mid);
        ++first, ++mid;
        if (first == old) old = mid; // left half exhausted
        else if (mid == last) mid = old;
    }
}

int main() {
    using std::cout;
    std::vector<int> v {0,1,2,3,4,5,6,7,8,9};
    cout << "before rotate: ";
    for (auto x: v) cout << x << ' '; cout << '\n';
    int k = 7;
    rotate_by_gcd_like_swap(v.begin(), v.begin() + k, v.end());
    cout << " after rotate: ";
    for (auto x: v) cout << x << ' '; cout << '\n';
    cout << "sz = " << v.size() << ", k = " << k << '\n';
}

وحافظ على مؤشرين للمجموعة، يبدأ مؤشر واحد من بداية مجموعة إلى نهاية مجموعة. يبدأ مؤشر آخر من موقع MTH من الماضي، وحلقات عبر العناصر M مشاركة أي عدد من المرات. يأخذ O (ن) في جميع الأوقات. لا مساحة إضافية مطلوبة.

circleArray(Elements,M){
 int size=size-of(Elements);

 //first index
 int i1=0;

 assert(size>M)

 //second index starting from mth position from the last
 int i2=size-M;

 //until first index reaches the end
 while(i1<size-1){

  //swap the elements of the array pointed by both indexes
  swap(i1,i2,Elements);

  //increment first pointer by 1
  i1++;

  //increment second pointer. if it goes out of array, come back to
  //mth position from the last
  if(++i2==size) i2=size-M;

 }
}

وانظر هذا إذا كنت ترغب في تطبيق جافا:

<وأ href = "http://programmingpearls.blogspot.com/search؟updated-min=2011-01-01T00٪3A00٪3A00-08٪3A00&updated-max=2012-01-01T00٪3A00٪3A00- 08٪ 3A00 & ماكس-النتائج = 2 "يختلط =" نوفولو "> البرمجة لآلئ: التعميم يسار / يمين التحول عملية

static int [] shift(int arr[], int index, int k, int rem)
{
    if(k <= 0 || arr == null || arr.length == 0 || rem == 0 || index >= arr.length)
    {
        return arr;
    }

    int temp = arr[index];

    arr = shift(arr, (index+k) % arr.length, k, rem - 1);

    arr[(index+k) % arr.length] = temp;

    return arr;
}

وروبي سبيل المثال:

def move_cyclic2 array, move_cnt
  move_cnt = array.length - move_cnt % array.length 
  if !(move_cnt == 0 || move_cnt == array.length)            
    array.replace( array[move_cnt..-1] + array[0...move_cnt] )  
  end   
end

في نظرية، أسرع واحد هو حلقة من هذا القبيل:

if (begin != middle && middle != end)
{
    for (i = middle; ; )
    {
        swap(arr[begin++], arr[i++]);
        if (begin == middle && i == end) { break; }
        if (begin == middle) { middle = i; }
        else if (i == end) { i = middle; }
    }
}

في الواقع، يجب أن ملف ونرى.

وهنا هو نوثر واحد (C ++):

void shift_vec(vector<int>& v, size_t a)
{
    size_t max_s = v.size() / a;
    for( size_t s = 1; s < max_s; ++s )
        for( size_t i = 0; i < a; ++i )
            swap( v[i], v[s*a+i] );
    for( size_t i = 0; i < a; ++i )
        swap( v[i], v[(max_s*a+i) % v.size()] );
}

وبطبيعة الحال فإنه لا يكاد أنيقة مثل حل العكسية ثلاث مرات الشهير، ولكن اعتمادا على الجهاز يمكن أن يكون similary بسرعة.

وcircleArray لديه بعض الأخطاء ولا يعمل في جميع الحالات!

وحلقة يجب أن تستمر while i1 < i2 NOT i1 < last - 1.

void Shift(int* _array, int _size, int _moves)
{
    _moves = _size - _moves;
    int i2 = _moves;
         int i1 = -1;
         while(++i1 < i2)
    {
        int tmp = _array[i2];
        _array[i2] = _array[i1];
        _array[i1] = tmp;
        if(++i2 == _size) i2 = _moves;
    }
}

وهناك صديق لي في حين يمزح طلب مني كيفية تحويل صفيف، خطرت لي هذه الحلول (انظر الرابط ideone)، لك الآن رأيت، شخص يبدو مقصور على فئة معينة قليلا.

ونلقي نظرة هنا .

#include <iostream>

#include <assert.h>

#include <cstring>

using namespace std;

struct VeryElaboratedDataType
{
    int a;
    int b;
};

namespace amsoft
{
    namespace inutils
    {
        enum EShiftDirection
        {
            Left,
            Right
        };
template 
<typename T,size_t len>
void infernalShift(T infernalArray[],int positions,EShiftDirection direction = EShiftDirection::Right)
{
    //assert the dudes
    assert(len > 0 && "what dude?");
    assert(positions >= 0 && "what dude?");

    if(positions > 0)
    {
    ++positions;
    //let's make it fit the range
    positions %= len;

    //if y want to live as a forcio, i'l get y change direction by force
    if(!direction)
    {
        positions = len - positions;
    }

    // here I prepare a fine block of raw memory... allocate once per thread
    static unsigned char WORK_BUFFER[len * sizeof(T)];
    // std::memset (WORK_BUFFER,0,len * sizeof(T));
    // clean or not clean?, well
    // Hamlet is a prince, a prince does not clean

    //copy the first chunk of data to the 0 position
    std::memcpy(WORK_BUFFER,reinterpret_cast<unsigned char *>(infernalArray) + (positions)*sizeof(T),(len - positions)*sizeof(T));
    //copy the second chunk of data to the len - positions position
    std::memcpy(WORK_BUFFER+(len - positions)*sizeof(T),reinterpret_cast<unsigned char *>(infernalArray),positions * sizeof(T));

    //now bulk copy back to original one
    std::memcpy(reinterpret_cast<unsigned char *>(infernalArray),WORK_BUFFER,len * sizeof(T));

    }

}
template 
<typename T>
void printArray(T infernalArrayPrintable[],int len)
{
        for(int i=0;i<len;i++)
    {
        std::cout << infernalArrayPrintable[i] << " ";
    }
    std::cout << std::endl;

}
template 
<>
void printArray(VeryElaboratedDataType infernalArrayPrintable[],int len)
{
        for(int i=0;i<len;i++)
    {
        std::cout << infernalArrayPrintable[i].a << "," << infernalArrayPrintable[i].b << " ";
    }
    std::cout << std::endl;

}
}
}




int main() {
    // your code goes here
    int myInfernalArray[] = {1,2,3,4,5,6,7,8,9};

    VeryElaboratedDataType myInfernalArrayV[] = {{1,1},{2,2},{3,3},{4,4},{5,5},{6,6},{7,7},{8,8},{9,9}};
    amsoft::inutils::printArray(myInfernalArray,sizeof(myInfernalArray)/sizeof(int));
    amsoft::inutils::infernalShift<int,sizeof(myInfernalArray)/sizeof(int)>(myInfernalArray,4);
    amsoft::inutils::printArray(myInfernalArray,sizeof(myInfernalArray)/sizeof(int));
    amsoft::inutils::infernalShift<int,sizeof(myInfernalArray)/sizeof(int)>(myInfernalArray,4,amsoft::inutils::EShiftDirection::Left);
    amsoft::inutils::printArray(myInfernalArray,sizeof(myInfernalArray)/sizeof(int));
    amsoft::inutils::infernalShift<int,sizeof(myInfernalArray)/sizeof(int)>(myInfernalArray,10);
    amsoft::inutils::printArray(myInfernalArray,sizeof(myInfernalArray)/sizeof(int));


    amsoft::inutils::printArray(myInfernalArrayV,sizeof(myInfernalArrayV)/sizeof(VeryElaboratedDataType));
    amsoft::inutils::infernalShift<VeryElaboratedDataType,sizeof(myInfernalArrayV)/sizeof(VeryElaboratedDataType)>(myInfernalArrayV,4);
    amsoft::inutils::printArray(myInfernalArrayV,sizeof(myInfernalArrayV)/sizeof(VeryElaboratedDataType));
    amsoft::inutils::infernalShift<VeryElaboratedDataType,sizeof(myInfernalArrayV)/sizeof(VeryElaboratedDataType)>(myInfernalArrayV,4,amsoft::inutils::EShiftDirection::Left);
    amsoft::inutils::printArray(myInfernalArrayV,sizeof(myInfernalArrayV)/sizeof(VeryElaboratedDataType));
    amsoft::inutils::infernalShift<VeryElaboratedDataType,sizeof(myInfernalArrayV)/sizeof(VeryElaboratedDataType)>(myInfernalArrayV,10);
    amsoft::inutils::printArray(myInfernalArrayV,sizeof(myInfernalArrayV)/sizeof(VeryElaboratedDataType));

    return 0;
}

وهذه الطريقة سوف تفعل هذا العمل:

public static int[] solution1(int[] A, int K) {
    int temp[] = new int[A.length];

    int count = 0;

    int orignalItration = (K < A.length) ? K :(K%A.length); 


    for (int i = orignalItration; i < A.length; i++) {
        temp[i] = A[count++];
    }
    for (int i = 0; i < orignalItration; i++) {
        temp[i] = A[count++];
    }

    return temp;
}

وعلى غرارIsaacTurner وليس أنيقة بسبب النسخ غير الضرورية، ولكن التنفيذ قصيرة جدا.

والفكرة - تبادل العنصر A على المؤشر 0 مع العنصر B التي تقع على جهة A. B الآن هو أولا. مبادلة مع العنصر C التي تقع على جهة B. تستمر حتى الوجهة ليست في 0.

إذا القاسم المشترك الأكبر ليست 1 ثم كنت لم تنته بعد - تحتاج إلى مواصلة مبادلة، ولكن الآن باستخدام مؤشر 1 في بلدكم البداية والنهاية نقطة

وتستمر حتى نقطة الانطلاق ليست هي GCD.

int gcd(int a, int b) => b == 0 ? a : gcd(b, a % b);

public int[] solution(int[] A, int K)
{
    for (var i = 0; i < gcd(A.Length, K); i++)
    {
        for (var j = i; j < A.Length - 1; j++)
        {
            var destIndex = ((j-i) * K + K + i) % A.Length;
            if (destIndex == i) break;
            var destValue = A[destIndex];
            A[destIndex] = A[i];
            A[i] = destValue;
        }
    }

    return A;
}

وهنا هو الحل بلدي في جاوة التي حصلت لي نتيجة العمل 100٪ و 100٪ الصواب في Codility:

class Solution {
    public int[] solution(int[] A, int K) {
        // write your code in Java SE 8
        if (A.length > 0)
        {
            int[] arr = new int[A.length];
            if (K > A.length)
                K = K % A.length;

            for (int i=0; i<A.length-K; i++)
                arr[i+K] = A[i];

            for (int j=A.length-K; j<A.length; j++)
                arr[j-(A.length-K)] = A[j];

            return arr;
        }
        else
            return new int[0];
    }
}

ملحوظة أنه على الرغم من رؤية اثنين من الحلقات for، التكرار على مجموعة كاملة ويتم مرة واحدة فقط.

وسويفت 4 نسخة لتحويل مجموعة اليسار.

func rotLeft(a: [Int], d: Int) -> [Int] {

   var result = a
   func reverse(start: Int, end: Int) {
      var start = start
      var end = end
      while start < end {
         result.swapAt(start, end)
         start += 1
         end -= 1
      }
   }

   let lenght = a.count
   reverse(start: 0, end: lenght - 1)
   reverse(start: lenght - d, end: lenght - 1)
   reverse(start: 0, end: lenght - d - 1)
   return result
}

وعلى سبيل المثال، إذا إدخال مجموعة هي a = [1, 2, 3, 4, 5]، وتحول اليسار الإزاحة d = 4، ثم ستكون النتيجة [5, 1, 2, 3, 4]

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top