عكس الفرز

يظهر مبرمج من الهند بوضوح Zig-Zag و Zig-Zig و Zig المستخدمة في خوارزمية SplaySort:


نستكشف هذا الموسم مجموعة متنوعة من الأكوام وكيف يمكن استخدامها للفرز . ومع ذلك ، هذه المرة سوف نبتعد عن الموضوع الرئيسي. هيكل اليوم - شجرة مباعدة - ليس حفنة. لكننا نحتاجها للاستعداد ذهنيًا لدراسة الكومة القادمة - في الأسبوع المقبل ستكون هناك محاضرة حول الفرز بواسطة شجرة ديكارتية.
إديسون برمجيات - تطوير الويب
تم إعداد هذه المقالة بدعم من إديسون. أحد اتجاهات نشاطنا هو أتمتة القياسات والأنظمة الخبيرة ، التي نقوم من خلالها بتنفيذ مشاريع عالية التقنية باستخدام نهج علمي صارم. نحن نحب علوم الكمبيوتر! ؛-)






فرز شجرة البحث الثنائية


شجرة Splay هي شجرة بحث ثنائية محسنة. أولاً ، لنتذكر كيفية الفرز باستخدام الشجرة الثنائية المعتادة "غير المحسنة".

كما تعلم جيدًا ، في شجرة ثنائية ، أي طفل يسار أقل من الوالد ، أي طفل أيمن ليس أقل (أي أكبر أو متساوٍ) من الوالد.


الفرز مباشر بشكل عام:

  • المرحلة 1. بناء على الصفيف ، قم ببناء شجرة بحث ثنائية. نضع العنصر الأول من الصفيف في الجذر ، نقارن أولاً العناصر المتبقية مع الجذر ، ثم ، بناءً على المقارنة ، ننتقل إلى أسفل الفرعين الأيسر أو الأيمن (وعلى طول الطريق الذي نقارن فيه عنصر الصفيف بعقد الشجرة الموجودة). في النهاية ، يصل العنصر التالي إلى نهاية الفرع ويصبح العقدة نفسها.
  • 2. . , ( , ) . .

بناء شجرة أمر مقبول تمامًا من حيث التعقيد الخوارزمي - في المتوسط ​​، يكلف إدخال عقدة جديدة O (log n ) ، وبالتالي فإن التعقيد الزمني للمرحلة الأولى هو O ( n log n ) .

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

شجرة Splay


لحل هذه المشكلة ، قبل حوالي 35-37 عامًا فقط ، طور عالما عالمان روبرت تارجان ودانيال سليتر هيكل الشجرة هذا. اقترحوا أنه بالنسبة لأي عمليات (إدراج ، بحث ، حذف) مع أي عقدة شجرة ، قم بإعادة التوازن على الفور للشجرة ، مما يجعل العقدة جذر الهيكل بأكمله.


في الصورة اليسرى روبرت تارجان (الصف الأول ، الثاني الأيمن) بصحبة مهندس ماتريكس ومخترع باسكال. في الصورة الصحيحة دانيال سليتور.
سيؤدي النقر فوق الصورة إلى فتح نسخة كاملة التنسيق.


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

من أجل دفع العقدة إلى الجذر ، يتم استخدام عمليات بسيطة خاصة ، ويسمى ما يلي:

منعرج التعرج


إذا كان العنصر الذي تريد تكوين جذر له في المستوى الثاني من التعشيش ، فإن كل شيء بسيط للغاية.

نحن دلالة على هذا العنصر مثل وX ، والأم (والذي هو أيضا جذر الشجرة) - كما P .
أ ، ب ، ج هي مقاطع فرعية. كم هو هناك في هذه العقد الأشجار الفرعية لا يهم، مهتمة فقط في جذور هذه الأشجار الفرعية التي لها علاقات "بين الوالدين والطفل" مع X و P . إذا كانت أي من الشرائط الفرعية مفقودة (أي إذا لم يكن لدى X و P أي أحفاد) ، فلن يؤثر ذلك على ترتيب الإجراءات.


لجعل X جذر الشجرة ، تحتاج إلى عكس العلاقة بين الوالدين والطفل بين X و P ، وتفوق أيضًا الشجرة الفرعية B - كان السليل الأيمن لـ X ، وأصبح السليل الأيسر لـ P ( لا تحتاج إلى القيام بأي شيء مع A و C ). وكل شيء! نتيجة لهذه التلاعبات البسيطة ، فإن البنية ، كما كانت شجرة بحث ثنائية ، ظلت كذلك - لن يتم انتهاك مبدأ "الطفل الأيسر أقل من الوالد ، والطفل الأيمن أكبر أو مساوٍ من الوالد".

الصورة السابقة X هو سليل الأيسر من P . إذا كانت X كان سليلًا حقًا ، ما عليك سوى عكس الموقف:


إذا لم يكن لدى X المستوى الثاني من الصعوبة ، ولكن المستوى الثالث ، فإن كل شيء أكثر تعقيدًا ، ولكن ليس كثيرًا.

إذا X لديها أحد الوالدين P ، والتي بدورها لديها أحد الوالدين G ، ثم لدينا سوى اثنين من حالات مختلفة:
1) إذا X هو سليل ل P على نفس الجانب كما P ل G .
2) إذا X و P هم أحفاد تنوعا لآبائهم.

أولا ، النظر في الحالة الأولى.

ZigZig Turn


دع X يكون السليل الأيسر لـ P ، و P السليل الأيسر لـ G (الخيار عندما يكون X و P في نفس الوقت أحفاد أحفاد يتم حلها بالمثل).


كما ترون ، من الضروري تغيير العلاقة بين X و P و G ، بالإضافة إلى تفوق الشريحتين B و C بشكل صحيح . وشجرة البحث الثنائية لدينا لن تعاني من ذلك.

تحول متعرج


إذا كانت X هي السليل الأيمن ، وتركت P (أو تركت X ، وكانت P على اليمين ، وليس الجوهر) ، فأرجو أن تكون قد فهمت بالفعل كل شيء من هذه الصورة:


إذا كانت X في الشجرة لا تزال في مستوى أقل من التعشيش ، فحينئذٍ لرفعها ، تحتاج إلى تطبيق ZigZig أو ZigZag (إذا لزم الأمر ، قم بذلك عدة مرات) على هذا الوالد في الفرع ، وهو في المستوى الثالث من التعشيش.

عكس الترتيب: فرز Splay


في الواقع ، نقوم هنا بتنفيذ نفس النقاط كما في فرز الأشجار - أولاً نقوم ببناء شجرة فرز ثنائية ، ثم نلتف حولها. ولكن في كل مرة ندخل عقدة جديدة في الشجرة - باستخدام التلال والقصور ، نجعلها الجذر.

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

ومع ذلك ، ونتيجة لذلك ، تحدث تحولات مذهلة مع الشجرة - فهي تستقيم وتبقى في مثل هذه الحالة المستقيمة طوال الوقت. وهذا يسرع بشكل حاسم المرحلة الثانية (اجتياز الأشجار) ، حيث يتم معالجة السلم النهائي الناتج بتعقيد O ( n ).

وبالتالي ، فإن التعقيد الكلي للخوارزمية (الأسوأ ، المتوسط ​​، الأفضل) في الوقت المناسب هو O ( n log n ) .

عمليًا ، هذا التصنيف أبطأ من MergeSort و QuickSort و HeapSort الآخر ، ولكنه يوضح بوضوح كيف يمكنك تسريع العمليات باستخدام شجرة البحث الثنائية.

المعنى الأساسي لهذا الخرافة هو: إذا كان عليك التعامل مع شجرة بحث ثنائية - إذا أمكن ، اجعلها متوازنة. كخيار ممكن - العمل معه كما هو الحال مع شجرة الانقسام ، أي في أي إجراء مع أي عقدة في الشجرة ، اجعل هذه العقدة الجذر. بالطبع ، هناك أشجار بحث بديلة أخرى ذاتية التوازن (شجرة حمراء-سوداء ، شجرة AVL ، إلخ) ، والتي قد تكون أكثر تفضيلاً.

كود C


لا تبدو عصبية
/*
 *------------------------------------------------------------
 *
 *      File..........: $RCSfile: splaysort.c,v $
 *      Revision......: $Revision: 1.2 $
 *      Author........: Gary Eddy (gary@cs.mu.OZ.AU)
 *			Alistair Moffat (alistair@cs.mu.OZ.AU)
 *      Date..........: $Date: 1995/09/07 06:19:17 $
 *
 *	Description:
 *		Sorts by repeated insertion into a splay tree.
 *		Insertions are done top down
 *
 *------------------------------------------------------------
 */

#include	<stdio.h>
#include	<stdlib.h>
#include	<malloc.h>
#include	<alloca.h>

char	*sort_name = "Splaysort";
char	*sort_version = "$Revision: 1.2 $";

/*
** Define DATAPTR for the 12 byte per record version of the program.
** Otherwise only 8 extra bytes per record are used and data
** references are done by indexing the data array.
** Different compiler/architecture combinations can cause wild
** variation in the ratio of speeds between these variations.
*/

#define DATAPTR

#ifdef DATAPTR
#define DATA(p) ((p)->data)
#else
#define DATA(p) (A+size*(p-T))
#endif

/*
** With the fast copy option enabled a more intelligent copy is used
** depending on the size of the items being copied.
** This approach adopted from the nqsort program of Bentley and McIlroy,
** see Software Practice and Experience, v23, n11, November 1993, 1249-65.
*/

#define FASTCOPY

#ifdef FASTCOPY
#define COPYCODE(TYPE, parmi, parmj, n) { \
        long i = (n) / sizeof (TYPE); \
        register TYPE *pi = (TYPE *) (parmi); \
        register TYPE *pj = (TYPE *) (parmj); \
        do { \
                *pi++ = *pj++;                    \
        } while (--i > 0);      \
}

void
copyfunc(char *d, char *s, int size, int copytype)
{
        if(copytype <= 1)
                COPYCODE(long, d, s, size)
        else
                COPYCODE(char, d, s, size)
	return;
}

#define COPY(d,s,size) \
        if (copytype == 0) { \
                *(long *)(d) = *(long *)(s); \
        } else \
                copyfunc(d, s, size, copytype)
#else
#define COPY(d,s,size)	memcpy(d,s,size)
#endif

typedef struct  node_rec node;

struct	node_rec {
	node	*left, *rght;
#ifdef DATAPTR
	char	*data;
#endif
};

/*
 *	sort():
 *		The entire sort code 
 *
 *	Accesses outside variables:	none
 *
 *	Return value...:		none
 */

void
sort(void *A, size_t n, size_t size, int (*cmp)(const void *, const void *))
{
	register node *next, *curr, *prnt;
	char 	*item;
	node	*l, *r, *ch;
	node	root, *T, *new, *end, *move;
#ifndef DATAPTR
	char	*p;
#endif

/*
** Determine which copying method will be used.
*/
#ifdef FASTCOPY
	int	copytype=((char *)A - (char *)0) % sizeof(long) ||
		size % sizeof(long) ? 2 : size == sizeof(long) ? 0 : 1;
#endif


	if(n < 2)
		return;
	if((T = new = (node *) malloc(sizeof(*T)*n)) == NULL) {
		fprintf(stderr, "Couldn't allocate space for structure\n");
	}
	/* 
	** Do the first insertion manually to avoid the empty tree
	** case in the main loop.
	*/
	curr = new++;
	item = A;
	curr->left = curr->rght = NULL;
#ifdef DATAPTR
	curr->data = item;
#endif
	item += size;
	end = T+n;
	/*
	** For each item move down the tree dividing it into
	** two subtrees, one containing items less than the new
	** element and the other those which are greater.
	** The pointers l and r refer to the greatest element in the
	** smaller subtree and the smallest element in the large
	** subtree respectively. During the splitting of the tree
	** l and r retain children that may be incorrect in the final
	** tree but these false links are cut at the end of the
	** insertion.
	*/

	for( ; new<end; ) {
		l = r = &root;
		while(curr != NULL) {
			if(cmp(item, DATA(curr)) < 0) {
				/* Left root case */
				if((ch = curr->left) == NULL) {
					r = r->left = curr;
					break;
				}
				/* Left zig-zig */
				else if(cmp(item, DATA(ch)) < 0) {
					curr->left = ch->rght;
					ch->rght = curr;
					r = r->left = ch;
					curr = ch->left;
				}
				/* Left zig-zag */
				else {
					r = r->left = curr;
					l = l->rght = ch;
					curr = ch->rght;
				}
			}
			else {
				/* Right root case */
				if((ch = curr->rght) == NULL) {
					l = l->rght = curr;
					break;
				}
				/* Right zig-zag */
				else if (cmp(item, DATA(ch)) < 0) {
					l = l->rght = curr;
					r = r->left = ch;
					curr = ch->left;
				}
				/* Right zig-zig */
				else {
					curr->rght = ch->left;
					ch->left = curr;
					l = l->rght = ch;
					curr = ch->rght;
				}
			}
		}
		/* Cut false links */
		r->left = l->rght = NULL;
		new->rght = root.left;
		new->left = root.rght;
#ifdef DATAPTR
		new->data = item;
#endif
		curr = new++;
		item += size;
	}
	/* Now copy all of the data back into the input array.
	** Uses an iterative destructive inorder traversal.
	** Last item inserted is the current root.
	*/
	prnt = NULL;
	move = T;
	while (1) {
		if ((next = curr->left) != NULL) {
			/* left subtree exists */
			curr->left = prnt;
			prnt = curr;
			curr = next;
		}
		else {
			next = curr->rght;
			curr->rght = move++;
			if (next != NULL) {
				/* and arrange for a visit */
				if((curr = next->left) != NULL) {
					next->left = prnt;
					prnt = next;
					continue;
				}
				else {
					curr = next;
					continue;
				}
			}
			/* no right subtree either, turn around*/
			if (prnt != NULL) {
				curr = prnt;
				prnt = prnt->left;
				curr->left = NULL;
				continue;
			}
			/* nothing left to be done */
			break;
		}
	}
#ifndef DATAPTR
	/*
	** Change the goes-to array in rght to a comes_from in left.
	** Note the kludge on pointers, where left points into the 
	** character array containing the elements.
	*/
	for(next = T, p = A; next < end; p += size, next++)
		next->rght->left = (node *)p;
	/* and use the `comes from' array to unscramble the permutation */
	item = (char *)malloc(size);
        for (next=T; next<end; next++) {
                char *datacurr, *dataleftcurr, *final;
                if (next->left == NULL)
                        continue;
                curr = next;
                final = datacurr = DATA(curr);
                COPY(item, datacurr, size);
                while ((char *)(curr->left) != final) {
                        dataleftcurr = (char *)(curr->left);
                        COPY(datacurr, dataleftcurr, size);
                        prnt = curr;
                        curr = T + (((char *)(curr->left)-A)/size);
                        prnt->left = NULL;
                        datacurr = dataleftcurr;
                }
                COPY(datacurr, item, size);
                curr->left = NULL;
        }
#else
	/* Change the goes-to array in rght to a comes_from in left */
	for(next = T; next < end; next++)
		next->rght->left = next;
	/* and use the `comes from' array to unscramble the permutation */
	/*
	** This 12 byte version uses the presence of the data pointer
	** by making it the flag for already placed items. This means
	** that left pointers can point to nodes, eliminating the
	** calculation to find the next node.
	*/
	item = (char *)malloc(size);
	for (next=T; next<end; next++) {
		char *datacurr, *dataleftcurr, *final;
		/* second condition guarantees at least one iteration
		** of while loop.
		*/
		if (DATA(next) == (char *) NULL || next->left == next)
			continue;
		final = datacurr = DATA(next);
		curr = next->left;
		COPY(item, datacurr, size);
		while ((dataleftcurr = DATA(curr)) != final) {
			COPY(datacurr, dataleftcurr, size);
			prnt = curr;
			curr = curr->left;
			DATA(prnt) = (char *) NULL;
			datacurr = dataleftcurr;
		}
		COPY(datacurr, item, size);
		DATA(curr) = (char *) NULL;
	}
#endif
	free(item);
	free(T);
} /* sort() */

مقطورة السلسلة التالية


ونعود إلى أكوام. الشيء الصغير التالي سيكون أكثر إثارة للاهتمام من الذي فحصناه اليوم. هذا الهجين عبارة عن بنية بيانات شبيهة بالشجرة وهي عبارة عن كومة بحث وشجرة بحث ثنائية.



المراجع


شجرة البحث الثنائية ، شجرة

Splay أشجار Splay

مقالات سلسلة:



تمت إضافة الفرز إلى AlgoLab. لذلك ، إذا قمت بتحديث ملف Excel باستخدام وحدات الماكرو ، فيمكنك إزعاج شجرة البحث الثنائية بشكل شخصي.

All Articles